Can you call a burst compiled function from native code?

I’m working on a native plugin for Unity, and I was wondering if it is possible to call a burst compiled function from native code?
Something like

public delegate byte SomeDelegate(float A, float B);

[BurstCompile]
public class SomeClass
{
    [BurstCompile, AOT.MonoPInvokeCallback(SomeDelegate)]
    public static byte PerformSomeCalculations(float A, float B)
    {
        // do something with it
        return output;
    }
}

public class SomeOtherClass
{
    [DllImport("MyPlugin")]
    public static extern void InitializeNativeFunction([In] FunctionPointer<SomeDelegate> callbackFunction);

    public static void InitializeSomeOtherClass()
    {
        var fnPtr = BurstCompiler.CompileFunctionPointer<SomeDelegate>(SomeClass.PerformSomeCalculations);

        InitializeNativeFunction(fnPtr);
        // FunctionPointer struct itself only contains a read-only IntPtr
        // which I assume is the actual burst-compiled function, because
        // the "Invoke" property (yes, property, not a function, uses
        // Marshal.GetDelegateForFunctionPointer to convert it into a delegate.
    }
}

My main aim with this kind of setup is to provide C# extensibility to my native code, but while typing this, another question popped in my head. So, in the end, I have two questions.

  1. Is this doable? Has someone done it before? Any gotchas I should be careful about?
  2. Will this actually reduce interop costs? Or end up increasing them instead?

As some more context, this is from burst docs:

And it’s what made me think of whether there would be an interop cost in

Okay, I did some basic tests, and I think it works, at least on Windows.

Here’s the source code:

using System.Globalization;
using System.Runtime.InteropServices;
using System.Security;
using Unity.Burst;
using UnityEngine;

namespace DefaultNamespace
{
    public delegate ulong CalculateFactorialDelegate(ulong input);

    public class BurstInteropTester : MonoBehaviour
    {
        [DllImport("MyDLL", CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurity]
        private static extern ulong SomethingAddFactorials(ulong a, ulong b);

        [DllImport("MyDLL", CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurity]
        private static extern void InitSomethingCalculateFactorialManagedOverride(CalculateFactorialDelegate function);

        [DllImport("MyDLL", CallingConvention = CallingConvention.StdCall), SuppressUnmanagedCodeSecurity]
        private static extern void InitSomethingCalculateFactorialManagedOverride(FunctionPointer<CalculateFactorialDelegate> function);

        [SerializeField] private ulong a = 0;
        [SerializeField] private ulong b = 0;
        [SerializeField] private bool useBurst = false;
        [SerializeField] private ulong calculationCount = 100;

        private void Awake()
        {
            InitSomethingCalculateFactorialManagedOverride(Library.CalculateFactorial);
            a = 2;
            b = 2;
            useBurst = false;
            calculationCount = 1000000;
        }

        private void OnGUI()
        {
            using (new GUILayout.HorizontalScope())
            {
                if (ulong.TryParse(GUILayout.TextField(a.ToString(CultureInfo.InvariantCulture)), out var aTemp))
                {
                    a = aTemp;
                }

                if (ulong.TryParse(GUILayout.TextField(b.ToString(CultureInfo.InvariantCulture)), out var bTemp))
                {
                    b = bTemp;
                }
            }

            var newValue = GUILayout.Toggle(useBurst, "Use Burst");
            if (useBurst != newValue)
            {
                useBurst = newValue;
                if (useBurst)
                {
                    var function = BurstCompiler.CompileFunctionPointer<CalculateFactorialDelegate>(Library.CalculateFactorial);
                    InitSomethingCalculateFactorialManagedOverride(function);
                }
                else InitSomethingCalculateFactorialManagedOverride(Library.CalculateFactorial);
            }

            if (ulong.TryParse(GUILayout.TextField(calculationCount.ToString(CultureInfo.InvariantCulture)), out var calcCountTemp))
            {
                calculationCount = calcCountTemp;
            }

            ulong result = 0;
            for (ulong i = 0; i < calculationCount; i++)
            {
                result = SomethingAddFactorials(a, b);
            }

            GUILayout.Label($"Result: {result}");
            GUILayout.Label($"FrameTime: {Time.deltaTime * 1000} milliseconds");
        }

        [BurstCompile]
        public static class Library
        {
            [BurstCompile]
            [AOT.MonoPInvokeCallback(typeof(CalculateFactorialDelegate))]
            public static ulong CalculateFactorial(ulong input)
            {
                var output = input;
                for (var i = input - 1; i > 1; --i)
                {
                    output *= i;
                }

                return output;
            }
        }
    }
}
struct Something
{
    static unsigned long long CalculateFactorial(unsigned long long Input);
    static unsigned long long AddFactorials(unsigned long long A, unsigned long long B);
};

unsigned long long (__stdcall*SomethingCalculateFactorialManagedOverride)(unsigned long long Input) = nullptr;

extern "C" void __stdcall InitSomethingCalculateFactorialManagedOverride(unsigned long long (__stdcall*InDelegate)(unsigned long long Input))
{
    SomethingCalculateFactorialManagedOverride = InDelegate;
}

unsigned long long Something::CalculateFactorial(unsigned long long Input)
{
    return SomethingCalculateFactorialManagedOverride(Input);
}

extern "C" unsigned long long __stdcall SomethingAddFactorials(const unsigned long long A, const unsigned long long B)
{
    return Something::AddFactorials(A, B);
}

unsigned long long Something::AddFactorials(const unsigned long long A, const unsigned long long B)
{
    return CalculateFactorial(A) + CalculateFactorial(B);
}

Results (in frame-time, rough averages):

  • Editor (Win64):

  • useBurst = false: 66ms

  • useBurst = true: 33ms

  • Standalone Player (Mono-Win64):

  • useBurst = false: 60ms

  • useBurst = true: 30ms

  • Standalone Player (IL2CPP-Win64):

  • useBurst = false: 30ms

  • useBurst = true: 30ms

Thinking about it, it’s not terribly surprising that IL2CPP does not get affected, but there’s still a significant improvement for Mono/Editor.