C# 9 function pointers - native-to-managed calls w/ IL2CPP?

I’ve been experimenting with C# 9 function pointers in Unity and, for the most part, have found they work well* for managed-to-native calls. However, I’ve been unable to get the reverse, native-to-managed calls, working under IL2CPP in particular.

Imagine a simple native library with a function set_callback that takes a function pointer in order to set a callback, and a function trigger_callback that causes invocation of the stored callback. And imagine I have simple C# DllImport bindings to these functions in the static class NativeLib (using void* as the callback type in the managed signatures*)*.

I can declare my static managed callback method, declare a matching delegate type, annotate the method with MonoPInvokeCallbackAttribute and then use GetFunctionPointerForDelegate and everything works as you’d expect using either Mono or IL2CPP:

delegate void CallbackDelegate();

[MonoPInvokeCallback(typeof(CallbackDelegate))]
static void Callback()
{
    Debug.Log("Callback!");
}

void Start()
{
    var callback = (void*)Marshal.GetFunctionPointerForDelegate<CallbackDelegate>(Callback);
    Debug.Log($"Setting callback: {(nint)callback}");
    NativeLib.set_callback(callback);
    Debug.Log("Triggering callback");
    NativeLib.trigger_callback();
}

However, if try to do the same with function pointers instead, IL2CPP players (only) will crash:

void Start()
{
    delegate* managed <void> callback = &Callback;
    Debug.Log($"Setting callback: {(nint)callback}");
    NativeLib.set_callback(callback);
    Debug.Log("Triggering callback");
    NativeLib.trigger_callback(); // crashes w/ IL2CPP!
}

Is there something special that needs to happen to make this work (ala using MonoPInvokeCallbackAttribute for IL2CPP compatibility in the delegate case*)*? Note that the function pointer approach works fine using Mono, and also under .NET 7 with Native AOT. I can’t declare my callback pointer as an unmanaged function pointer without the UnmanagedCallersOnlyAttribute from .NET 5+, but in any case, I understood that to be optional (and things seems to be fine without it in .NET 7 Native AOT).

*I have to explicitly declare a calling convention on managed-to-native function pointers since “The target runtime doesn’t support extensible or runtime-environment default calling conventions”, which is weird, since any of the built in conventions seem to work even though they aren’t relevant on most platforms, so seemingly some sort of default is being used. Any way around this?

I’m not entirely sure, so don’t quote me on this, but I believe that ‘UnitySendMessage’ is the ‘intended’ way to do callbacks from C++. So that would be your best bet instead of direct callback-pointers, and should work on all platforms & back-ends. Anything outside of that is you prototyping with the engine to see if it will work for the platform you’re on. This is also what iOS- & Android-Plugins normally use.

Haven’t used this feature but I think the managed calling convention is for when you want to call the method from managed code. Instead, you should use the calling convention that matches your native side. Look at the example from the UnmanagedCallersOnlyAttribute, which appears to be the .Net 5+ equivalent of MonoPInvokeCallback. If that doesn’t work, then maybe this isn’t yet supported in Unity/Mono.

Edit: The default unmanaged calling convention seems to be unmanaged ext*, which is not yet supported by Unity.

(* “With no modopts, unmanaged ext is the platform default calling convention, unmanaged without the square brackets.”)

From the IOS-PluginDocs:

Unity - Manual: Building plug-ins for iOS (unity3d.com)

8829160--1202167--upload_2023-2-23_13-4-46.png

As far as I can tell he’s using it correctly (for delegates)?
But again: You can always fall back to the (limited) UnitySendMessage

There is no safe way to pass a managed function pointer to native code. A managed function pointer must only be called from managed code. It happens not to crash because on Mono and CoreCLR because the managed calling convention for this method happens to match the native calling convention, but that isn’t a requirement.

But even if the calling convention matches, it is still not safe. CoreCLR has a cooperative GC and it switches modes when transition from native->managed and managed->native. But your managed function pointer will assume the GC is in cooperative mode and won’t make the transition when calling from native back into managed code. The callback will be running managed code in an invalid GC state. It will be sporadic, but it will eventually crash.

So you must only pass unmanaged function pointers to native code. The only way to do this in IL2CPP is to use GetFunctionPointerForDelegate. UnmanagedCallersOnlyAttribute works as well but it’s not yet supported in IL2CPP.

3 Likes

Thanks. I figured this was probably the case but since it worked in Mono and CoreCLR I wondered if maybe if it was possible somehow. But as you say, this was seemingly by luck rather than by design. I’ll stick to GetFunctionPointerForDelegate in the meantime.

When, if ever, can we expect IL2CPP support for UnmanagedCallersOnlyAttribute?

1 Like

I hope unity can enhance p/invoke services ASAP like what CoreCLR has done, MonoPInvokeCallback and GetFunctionPointerForDelegate need a bunch of redundant codes, it’s hard to wrap cpp libraries, which is really a treasure island.

UnmanagedCallersOnlyAttribute will probably be available when they switch to .NET8+/CoreCLR (work in progress). You can follow the thread here (and maybe ask your question there, Josh Peterson is awesome and replies to nearly all questions) Unity Future .NET Development Status