Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question NRE With EntityQuery - How can a struct even be null?

Discussion in 'Entity Component System' started by jwvanderbeck, May 20, 2023.

  1. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Ran into this today and spent far too long chasing my own tail until I figured out my mistake.

    Somehow I was getting a standard NRE when trying to use an EntityQuery I was building. Even calling query.IsEmpty would thrown an NRE.

    Which frankly confused the HECK out of me as an EntityQuery is a struct so how can it be "null" in the first place? Is this some deep dark C#'ism I'm not familiar with or is the error somehow completely misleading the user?

    I figured out what was causing my error, and I'll show that in a sec, but this is how it was originally setup (with some non-relevant code snipped)

    Code (CSharp):
    1.     public partial struct IdleCommandExecutionSystem : ISystem
    2.     {
    3.         private EntityQuery parentAgentQuery;
    4.      
    5.         [BurstCompile]
    6.         public void OnCreate(ref SystemState state)
    7.         {
    8.             state.RequireForUpdate<RandomStateComponent>();
    9.             state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
    10.          
    11.             var parentAgentQuery = new EntityQueryBuilder(Allocator.Temp)
    12.                 .WithAll<GenericCommandComponent, GenericCommandIndexComponent>()
    13.                 .WithAll<ParentAgentComponent>()
    14.                 .Build(state.EntityManager);
    15.  
    16.         }
    17.  
    18.         [BurstCompile]
    19.         public void OnUpdate(ref SystemState state)
    20.         {
    21.             var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
    22.                 .CreateCommandBuffer(state.WorldUnmanaged);
    23.             foreach (var (command, entity) in
    24.                      SystemAPI.Query<RefRO<IdleCommandComponent>>().WithEntityAccess())
    25.             {
    26.                 // currently this command does nothing, but is constantly "finishing" which
    27.                 // causes the agent to look for a new command
    28.              
    29.                 // Look for any waiting entity commands
    30.                 if (parentAgentQuery.IsEmpty)
    31.                 {
    32.                     Debug.Log("Parent query is empty");
    33.                 }
    34.  
    This would throw an NRE in the console on the if (parentAgentQuery.IsEmpty) line

    My error of course, if you didn't spot it, was I was making a private field on the struct "parentAgentQuery" and then turning around and making it a local variable when actually creating the query (with the same name so it was hiding it). Obviously stupid mistake in hindsight, but the point of this post is it feels like the error I was getting from Unity was extremely confusing and misleading. And to even get that error I had to disable Burst compilation.
     
  2. Spy-Master

    Spy-Master

    Joined:
    Aug 4, 2022
    Posts:
    274
    The NullReferenceException will happen when trying to dereference a null pointer to an unmanaged type as well. Struct properties and fields (in this case, apparently the EntityQuery.IsEmpty property) can always have implementations that call out to something needing a struct pointer or just a managed object somewhere. Exceptions will be thrown from that site (what else do you want it to do?).

    I meant to say struct properties and methods, not strict properties and fields. Fields are of course just attached data.
     
    Last edited: May 21, 2023
  3. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    How do you even have a pointer to an unmanaged type?
    EDIT: Sorry what I mean to say is how is a datatype defined as a struct turning into a pointer?

    I guess this is some c# black magic
     
    Last edited: May 20, 2023
  4. Spy-Master

    Spy-Master

    Joined:
    Aug 4, 2022
    Posts:
    274
    Nobody said that.

    EntityQuery is a proxy struct backed by an underlying framework-managed data structure.
    https://github.com/needle-mirror/co...Unity.Entities/Iterators/EntityQuery.cs#L3166
    Pretty much all the public members you see are properties and methods that perform indirection to the pointer which could be null for an invalid (e.g. default) EntityQuery. That helps keep the EntityQuery struct itself lightweight and resistant to possible corruptions doing struct copies since all the data still remains in the underlying struct.

    None of this is particularly black magic, it’s how fields and pointers have worked for decades.

    Previous reply incorrect: meant to say properties and methods, not properties and fields.
     
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Lots of structs internally contain reference pointers (as mentioned);
    Things like EntityQuery, EntityManager, ECB, Lookups, native containers etc all operate as reference based type. Like an object that does not allocate GC [depending on the context].

    That is because actual structure does not contain block of data at all.
    It contains pointers to the data. And pointers are copied as values.
    As a result, struct acts like being referenced "object" instead of just struct copied by value.

    While aux data (like Length or allocator type) may not be copied in some cases, the pointer to the allocated data block is still the same after struct copy.

    Meaning if you haven't initialized EntityQuery - like in the example - it will throw null ref exception.
    Since actual pointer leads nowhere.
    Which is usually a sign that you haven't initialized something, or allocated, or something was deallocated.

    Usually those exceptions are manually written throws to avoid hard crashes.
    You can check that by inspecting underlying code. Sometimes they are not.


    TL;DR:
    There is no difference when object becomes a null, or unmanaged pointer becomes null.
    In the end they're both pointers even if hidden internally by C#.
     
    Last edited: May 21, 2023
  6. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    When something can be null but you can't CHECK that it's null (IE query == null isn't possible), I still say its black magic :p
     
  7. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    How do you check the underlying code? That would be helpful. I had one error that was throwing on a completely different line because of the generated code, and was unsure how to see that.
     
  8. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Many of those struct have something like .IsCreated property which allows you to check if it was previously allocated.

    In the case of EntityQuery - manual disposal is not required because its lifecycle is bound to the system.
    Disposal happens automatically with registered respective system.
    Since usual use case for it - allocate in OnCreate & forget. Less typing.
    EntityQuery registered with EntityManager (I think) are disposed during World disposal.

    So, I'd say general rule is - if its IDisposable - it may not be allocated.

    As for the magic or not, its closer to C++ rather than C#. Hence, HPC# subset name.
    In general quirky, but you'll get used to it over time.

    Rider allows you to inspect both generated and actual code.
    If you're using VS then you can open underlying class manually and see whats going on.
    That's the beauty of packages. All code is available to you by default.
     
    Last edited: May 21, 2023
  9. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Oh I thought you mean the code generated by DOTS from our own raw code. That's what I was looking for :) Yeah I know thew package code is there of course.
     
  10. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Yeah I couldn't find one of those. I know it is common in the "normal" side of Unity.

    It is all these things together that just made the error message very confusing and less helpful than it otherwise might have been.
     
  11. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    There isn't much sense in making one for this struct though. (see my edited post)

    Since you shouldn't really check against if query exists.
    If context [system or MB] exists - EntityQuery should exist as well.
    Otherwise something went wrong. Therefore an exception.

    It may be a common issue when EntityQuery-s are created via MonoBehaviours / outside of systems.
    Since in some cases they may not be initialized correctly.
    But otherwise - there aren't that many uses cases to have IsCreated to check against.

    In any case, current API is a subject to change, so UT may add it somewhere in the future.