Detecting .NET runtime at compile time

.NET Runtimes while giving absolutely simillar API are internally different. For instance, Mono is written from scratch, while beight binary simmilar to the .NET Framework (I’ll call it NETFX) .NET/Core is written from scratch too, but follows NETFX much more, than Mono in the terms of internal layouts.

When you want to have single DLL which works in all .NET runtimes (Mono/IL2CPP/Burst/NETFX/NETCore), it’s crucial to detect which runtime you are currently in. While there are some dynamic methods, like query runtime name and check it for “Mono” string, it’s not the best approach, and definitely have not the best performance.

One of the main difference between “Real dotnet” (.NETFX, .NET/Core) and Mono family (Mono/IL2CPP) is object pointers. Real dotnet using +IntPtr.Size offset from the real object start, while Mono doesn’t apply any offsets. So for unsafe access to internal data we should consider those changes.

Long story short

I don’t want to bother you with the details of how did I get it. Though the solution is simple.

Detect .NETFX AND Mono

/// <returns>True if we are in .NET Framework and not in .NET/Core. Also returns true for Mono and IL2CPP</returns>
public static bool IsNetFrameworkAndNotCore()
{
    int size = Unsafe.SizeOf<AsyncVoidMethodBuilder>();
    return (size / IntPtr.Size) == 4;
}

Here we just check the size of managed (!) structure AsyncVoidMethodBuilder. In .NETFX and Mono this would be equal to IntPtr.Size * 4, while in .NET Core family it would be IntPtr.Size * 2

Differentiate .NET Family from Mono Family

public static unsafe bool IsMonoOrIl2cpp()
{
    return (sizeof(TypedReference) / sizeof(IntPtr)) == 3;
}

In the real dotnet sizeof(TypedReference) would be equal to IntPtr.Size*2 while in Mono/IL2CPP it would be equal to IntPtr.Size*3

Bonus point for Burst:

While it’s absolutely easy to detect the Burst itself (check out Unity Burst docs), we should also return correct runtime. In Burst you can’t get the size of managed types, even structs, so we should disable this check.

/*
Unfortunately, we can't use real detection in Burst, 
so we simulate that via this flag. 
For now, Unity is using Mono, 
it means that all internal layouts of object in C# 
would be equals to Mono's ones.
*/
public const bool IsDefaultNetFX = true; 

[BurstDiscard]
internal static void GetNetFrameworkSizeHint(ref int size)
{
    size = Unsafe.SizeOf<AsyncVoidMethodBuilder>();
}

/// <returns>True if we are in .NET Framework and not in .NET/Core. Also returns true for Mono and IL2CPP</returns>
public static bool IsNetFrameworkAndNotCore()
{
    int size = IsDefaultNetFX ? (IntPtr.Size * 4) : (IntPtr.Size * 2);
    GetNetFrameworkSizeHint(ref size);
    return (size / IntPtr.Size) == 4;
}

This solution will skip size-check, and in the Burst runtime will fallback to .NETFX.

Source code here: GitHub

That’s all for now, next article would be about .NET object layouts (such as string, object, T[], List<T>) in different runtimes and how we can make it work together.

Leave a Reply

Your email address will not be published. Required fields are marked *