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.
Is this doable? Has someone done it before? Any gotchas I should be careful about?
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.