Search Unity

Bug [IN-43029] Burst compiled code cannot recognize null references

Discussion in 'Burst' started by FaithlessOne, Jun 5, 2023.

  1. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    I want to report a Burst bug causing Burst compiled code not to detect null references. The following code is sufficient to reproduce the bug:

    Code (CSharp):
    1.  
    2.   [BurstCompile]
    3.   public partial struct TestSystem : ISystem
    4.   {
    5.     private bool _isFirstTime;
    6.    
    7.     [BurstCompile]
    8.     public unsafe void OnCreate(ref SystemState state)
    9.     {
    10.       this._isFirstTime = true;
    11.     }
    12.  
    13.     [BurstCompile]
    14.     public void OnUpdate(ref SystemState state)
    15.     {
    16.       if (this._isFirstTime)
    17.       {
    18.         this._isFirstTime = false;
    19.  
    20.         ref var nullReference = ref UnsafeUnmanaged.NullRef<AnotherStruct>();
    21.  
    22.         if (UnsafeUnmanaged.IsNotNullRef(ref nullReference))
    23.         {
    24.           Debug.LogError("IsNotNullRef: This code should not be called. Null reference not recognized.");
    25.         }
    26.  
    27.         if (UnsafeUnmanaged.IsNotNull(in nullReference))
    28.         {
    29.           Debug.LogError("IsNotNull: This code should not be called. Null reference not recognized.");
    30.         }
    31.       }
    32.     }
    33.   }
    34.  
    35.   [BurstCompile]
    36.   public static unsafe class UnsafeUnmanaged
    37.   {
    38.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    39.     public static bool IsNotNull<T>(in T source) where T : unmanaged
    40.     {
    41.       fixed (void* sourcePtr = &source)
    42.       {
    43.         return sourcePtr != (void*)0;
    44.       }
    45.     }
    46.  
    47.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    48.     public static bool IsNotNullRef<T>(ref T source) where T : unmanaged
    49.     {
    50.       return Unity.Collections.LowLevel.Unsafe.UnsafeUtility.AddressOf(ref source) != (void*)0;
    51.     }
    52.  
    53.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    54.     public static ref T NullRef<T>() where T : struct
    55.     {
    56.       return ref Unity.Collections.LowLevel.Unsafe.UnsafeUtility.AsRef<T>((void*)0);
    57.     }
    58.   }
    59.  
    60.   public struct AnotherStruct
    61.   {
    62.   }  
    The debug log error message are both shown when executing the code with Burst compilation enabled. Without Burst compilation code works without logging the errors.

    Created a reproduction project in IN-43029.
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,981
    My eyes bleed looking at this. I am actually more worried about the things you could do with this if it worked. It's bad enough to hear that such a "let's screw that nail with a Kaercher" thing works without Burst. :D

    Surely there's got to be a better way to design whatever use-case this is meant for? For instance, in a possibly similar situation where I need to know if a struct is "uninitialized" that struct stores an Index (plus other data). So I designed the code to consider any struct with a specific index (zero, any negative, int.MinValue, whatever) as "uninitialized". Begone, nullable-valuetype nullcheck!
     
  3. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    I agree with you. I also prefer using structs and detecting whether they are initialized or not instead of using null referencing structs like in my example. They may cause null reference exceptions, so they also make more trouble.

    But in some situations you want a method to return a pointer to a struct. Either to enable manipulating the struct data directly or to avoid copying large structs having KB sizes along the stack. Ref returning structs is the closest C# feature without directly using pointers for this. But what to do in cases when simply no reference to return is available? The options I see are either ref return a null reference (like the intention in my example), ref return a just created empty struct or ref return an already predefined empty struct. The last option enables the caller to change of the predefined struct, so quite dangerous. For ref returning a just created empty struct in plain C# you had to create the struct on the heap using an array. For being Burst compatible a native container would be required instead as far as I know. So pretty much overhead for only telling the caller that there is no reference to a struct. So I personally use detecting structs for being initialized as far as possible, but when a pointer to a struct is appropriate then I to stick with the first option and try to avoid to return null referencing structs.
     
  4. MarcoPersson

    MarcoPersson

    Unity Technologies

    Joined:
    Jul 21, 2021
    Posts:
    53
    Hi @FaithlessOne,

    This is technically not a bug, because refs (also called managed pointers in the VES) are never allowed to be null (See the spec: ECMA 335, section II.14.4.2 "Managed pointers"). If you break that invariant you'll get undefined behavior. When running without Burst, that behavior happens to be what you want. But Burst relies on the assumption (that a ref is never null) to perform certain optimizations.

    If you still want nullable references (which I don't necessarily recommend), you can create a wrapper struct around a pointer instead, like this:

    Code (CSharp):
    1. unsafe struct NullableRef<T> where T: unmanaged {
    2.     private T* _inner;
    3.  
    4.     public static NullableRef<T> Null
    5.         => new NullableRef<T> { _inner = (T*)0 };
    6.  
    7.     public static NullableRef<T> Just(ref T x)
    8.         => new NullableRef<T> { _inner = (T*)Unity.Collections.LowLevel.Unsafe.UnsafeUtility.AddressOf(ref x) };
    9.  
    10.     public bool IsNull => _inner == (T*)0;
    11.  
    12.     public ref T UnsafeAsRef()
    13.         => ref Unity.Collections.LowLevel.Unsafe.UnsafeUtility.AsRef<T>((void*)_inner);
    14. }
    15.  
    As long as you don't call UnsafeAsRef when it is null, you should get the expected behavior.
     
    Miro_Brodlova and CodeSmile like this.
  5. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    Thank you for the explanation, @MarcoPersson. I understood. When even the ECMA specs are stating that null references for managed pointers are not allowed, then I will give up this null reference approach. But still thanks for posting a workaround.

    Feel free to reject my bug report/incident then.
     
    MarcoPersson likes this.