Last active
September 7, 2023 22:01
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
// ----------------------------------------------- // | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 code for (a is null) evalutation:
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!