Skip to content

Instantly share code, notes, and snippets.

@atcarter714
Last active September 7, 2023 22:01
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save atcarter714/16940787825a56f82fac22aa967e4a75 to your computer and use it in GitHub Desktop.
Save atcarter714/16940787825a56f82fac22aa967e4a75 to your computer and use it in GitHub Desktop.
Clearing up the confusion about how null-ness of Unity Objects works with a very simple simulation of it!
#include <stdlib.h>
#define DllExport __declspec( dllexport )
// Some simple native C code to simulate how Unity does its magic in native code:
DllExport void* __cdecl allocateMem( size_t sizeInBytes ) {
return malloc( sizeInBytes );
}
DllExport void* __cdecl allocateMemFor( size_t count, size_t size ) {
return calloc( count, size );
}
DllExport void* __cdecl reallocateMem( void* ptr, size_t newSize ) {
return realloc( ptr, newSize );
}
DllExport void __cdecl deleteMem( void* pMem ) {
free( pMem );
}
DllExport size_t __cdecl getAllocSize( void* pAllocated ) {
return _msize( pAllocated );
}
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// Here we simulate a managed object using native memory resources which has the same
// behavior as a Unity Object with "null-ness" and checks. It demonstrates why if(obj)
// is usually how you should be doing your checks ...
internal class Program
{
private static void Main( string[] args )
{
// Create simulated Unity object:
UnityObjectWay? obj = new( "Hello, World, from the unmanaged heap!" );
WriteInColor( "Created object, now calling obj.Print() ...\n", ConsoleColor.Cyan );
// Call its public method and then evaluate it:
obj.Print();
Evaluate( obj );
// Now call Destroy on it and evaluate it again:
WriteInColor( "\nNow calling Destroy!", ConsoleColor.Red );
UnityObjectWay.Destroy( obj );
Evaluate( obj );
// Try it by assigning null directly:
WriteInColor("\nNow assigning null (literal) to obj:", ConsoleColor.Magenta );
obj = null;
Evaluate( obj );
}
public static void Evaluate( UnityObjectWay? obj )
{
var lineColor = ConsoleColor.Gray;
WriteInColor("\n----------------------------------", lineColor );
WriteInColor( "Trying if( obj ) check:" );
WriteInColor("----------------------------------", lineColor );
if ( obj )
WriteInColor( "\tobj evaluated as non-null!\n", ConsoleColor.DarkGreen );
else WriteInColor( "\tobj evaluated as null!\n", ConsoleColor.DarkRed );
WriteInColor("----------------------------------", lineColor);
WriteInColor("Trying if( obj != null ) check:");
WriteInColor("----------------------------------", lineColor);
if ( obj != null )
WriteInColor("\tobj evaluated as non-null!\n", ConsoleColor.DarkGreen );
else WriteInColor("\tobj evaluated as null!\n", ConsoleColor.DarkRed );
WriteInColor("----------------------------------", lineColor );
WriteInColor( "Cast to real C# object and check:" );
WriteInColor( "----------------------------------", lineColor );
var sysObj = (object?)obj;
if ( sysObj is not null )
WriteInColor( "\tIt's not null in C# terms ...\n", ConsoleColor.DarkGreen );
else WriteInColor( "\tIt's null for real!\n", ConsoleColor.DarkRed );
WriteInColor( "____________________________________________________________________________", ConsoleColor.DarkGray ); ;
}
public static void WriteInColor( string text, ConsoleColor color = ConsoleColor.White ) {
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ForegroundColor = originalColor;
}
}
public static unsafe class PInvoke
{
const string mylibName = "mylib.dll";
// --- Native DLL Functions:: --------------------- //
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern IntPtr allocateMem( ulong size );
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern IntPtr allocateMemFor( ulong count, ulong size );
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern IntPtr reallocateMem(IntPtr ptr, ulong newSize);
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern ulong getAllocSize(IntPtr pAllocated);
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern void deleteMem( IntPtr pMem );
// ----------------------------------------------- //
};
public unsafe class UnityObjectWay
{
const short INLINE = 0x100;
static uint id_counter = 0;
public UnityObjectWay( string str )
{
if( string.IsNullOrEmpty(str) )
throw str is null ?
new ArgumentNullException( nameof( str ) ) :
new ArgumentException( "Cannot allocate empty string!", nameof( str ) );
var pHeap = PInvoke.allocateMemFor( (ulong)str.Length, sizeof(char) );
if( pHeap == nint.Zero ) throw new InsufficientMemoryException("Allocation failed!");
this.ptr = (char *)pHeap;
this.count = str.Length;
this.sizeInBytes = charSize * count;
this.instanceID = id_counter++;
fixed ( char* pStr = str ) {
_writeToHeap( pStr, str.Length );
}
}
char* ptr;
int count;
readonly int sizeInBytes;
readonly int charSize = sizeof(char);
readonly uint instanceID = 0x00000000u;
// Helper methods for using native memory:
[MethodImpl(INLINE)] bool _writeToHeap( char[] chars ) => _writeToHeap( chars.AsSpan() );
[MethodImpl(INLINE)] bool _writeToHeap( char* pStr, int len ) => _writeToHeap( new Span<char>( pStr, len ) );
[MethodImpl(INLINE)] bool _writeToHeap( Span<char> pStr ) => pStr.TryCopyTo( new( ptr, count ) );
[MethodImpl(INLINE)] void _deleteBlock() => PInvoke.deleteMem( (IntPtr)ptr );
public void Print( ConsoleColor color = ConsoleColor.DarkYellow ) {
if ( ptr is null )
throw new NullReferenceException( "Object is null!" );
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = color;
for( int i = 0; i < count; ++i )
Console.Write( ptr[ i ] );
Console.Write('\n');
Console.ForegroundColor = originalColor;
}
// --- Simulates how Unity Object nullability works: ------------- //
public static void Destroy( UnityObjectWay obj ) {
if ( !IsNativeObjectAlive(obj) ) return;
obj._deleteBlock();
obj.ptr = null;
}
static bool IsNativeObjectAlive( UnityObjectWay? obj ) {
if ( obj is null ) return false;
if ( obj.ptr != null ) return true;
return false;
}
static bool CompareBaseObjects( UnityObjectWay? a, UnityObjectWay? b ) {
bool aNull = a is null, bNull = b is null;
if ( bNull & aNull ) return true;
if ( bNull ) return !IsNativeObjectAlive(a);
return aNull ? !IsNativeObjectAlive(b) : a?.instanceID == b?.instanceID;
}
public static implicit operator bool( UnityObjectWay obj ) =>
!CompareBaseObjects( obj, (UnityObjectWay?)null );
public static bool operator ==( UnityObjectWay? a, UnityObjectWay? b ) =>
CompareBaseObjects( a, b );
public static bool operator !=( UnityObjectWay? a, UnityObjectWay? b ) =>
!CompareBaseObjects( a, b );
};
public static extern ulong getAllocSize(IntPtr pAllocated);
[DllImport(mylibName, CharSet = CharSet.Unicode)]
public static extern void deleteMem( IntPtr pMem );
// ----------------------------------------------- //
};
@atcarter714
Copy link
Author

atcarter714 commented Dec 4, 2022

A further caveat I didn't even think of until now, as I became aware of it, is that Roslyn uses a "proper" null comparison when she encounters the (obj is null) style expression. It boils down to the simple ldarg, ldnull, ceq instructions: load the arg, load "null" and then do a conditional push of 0x1 (true) or 0x0 (false) ... that's the way the gods intended an evaluation of null-ness to work, haha! But there's a little nuance to be aware of with == and != because you can overload those on a user-defined type (as shown in the example) and override the default/built-in behavior. If a user-defined overload for == and != exists, then (obj == null) will actually be calling that instead of doing the "true null-ness" evaluation! And the result is gonna be whatever that user-defined operator wants it to be ...

This actually compiles to different IL code, which will look like this:

IL code for (a == null) evaluation:

IL_0010:  ldnull
IL_0011:  call bool A::op_Equality(class A, class A)
IL_0016:  stloc.0

IL code for (a is null) evalutation:

IL_0017: ldnull
IL_0018: ceq
IL_001a: stloc.1

So bear this little caveat/nuance in mind when checking the "null-ness" of something like "Object" types in Unity, or anything which has its own special overloads for == and != operators!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment