Mateusz
Mateusz

Calling managed code from Burst

Here’s a useful trick for when you’re way too deep in the Burst rabbit hole.

Unless you have the luxury of taking the time to (re)write your entire project with the Job System and Burst in mind, you might end up stuck in a Burst context, but needing to call a managed method.

Yes, you’re reading correctly. I’m talking about calling Mono code from Burst, not the other way around.

Maybe you have some managed code that you can’t easily rewrite for some reason. Maybe it’s part of a plugin that you’d rather not touch, or stuck in some cursed .dll. Or it could be a Unity function that doesn’t support Burst at all.

Thankfully, a lot of the UnityEngine.Physics API methods work with Burst out-of-the-box. Unfortunately, there’s a few exceptions that require workarounds. In the example below, we’ll use Physics.ComputePenetration which is extremely useful when working with PhysX colliders.

The method signature looks like this:

0
1
2
3
4
5
6
7
8
9
public static bool ComputePenetration(
    Collider colliderA, // ono a collider
    Vector3 positionA,
    Quaternion rotationA,
    Collider colliderB, // onooo another one
    Vector3 positionB,
    Quaternion rotationB,
    out Vector3 direction,
    out float distance
);

As you can see, it takes managed colliders as arguments, so it definitely won’t work directly with Burst. We can, however, create a function pointer to a wrapper method, and use a workaround for passing the collider arguments. It’s kinda like the opposite of Burst-compiled function pointers.

Note that we’ll only be able to get this example to work with Burst-compiled functions, not the Job System.0 Jobs are limited to using only thread-safe Unity APIs, of which there’s very few.

Referencing UnityEngine.Objects in a Burst context

Unfortunately, there is no way to use Unity objects1 with Burst directly. As a workaround, I suggest using InstanceIDs. These are non-deterministic, so definitely don’t store them anywhere, but they’re sufficient for indirectly identifying a specific object (of any type inheriting from UnityEngine.Object) for the duration of its lifetime.

Getting an object’s InstanceID is easy:

int id = collider.GetInstanceID();

However, for some reason, there’s no built-in way to get the object back based on its InstanceID in the public Unity API - instead, I’m using reflection to expose an internal UnityEngine.Object method. A more stable/responsive/responsible implementation could map objects to IDs using a dictionary, or something.

0
1
2
3
4
5
6
7
8
9
10
11
12
static class UnityEngineObjectUtility
{
    /// delegate based on an internal method in UnityEngine.Object: <see cref="UnityEngine.Object.FindObjectFromInstanceID"/>
    static readonly Func<int, UnityEngine.Object> findObjectFromInstanceId
        = (Func<int, UnityEngine.Object>)
        typeof(UnityEngine.Object)
            .GetMethod("FindObjectFromInstanceID", BindingFlags.NonPublic | BindingFlags.Static)
            .CreateDelegate(typeof(Func<int, UnityEngine.Object>));

    /// <summary> Get object instance based on its instance ID. See also: <see cref="UnityEngine.Object.GetInstanceID"/> </summary>
    public static TObject FindObjectFromInstanceID<TObject>(int instanceId) where TObject : UnityEngine.Object
        => findObjectFromInstanceId.Invoke(instanceId) as TObject;
}

Behold, boilerplate

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
sealed class ComputePenetrationBurstCompatible
{
    // the function pointer
    // this is where the pointer to the managed function is stored in a way accessible to burst.
    static readonly SharedStatic<FunctionPointer<ComputePenetrationDelegate>> SharedStaticFunctionPointer
        = SharedStatic<FunctionPointer<ComputePenetrationDelegate>>.GetOrCreate<ComputePenetrationBurstCompatible>();

    // delegate type of the function pointer
    // can't use System.Func/Action
    delegate void ComputePenetrationDelegate(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance
    );

    /// <summary>
    /// Burst-compatible wrapper for <see cref="Physics.ComputePenetration"/>.
    /// Computes the minimal translation required to separate the given colliders apart at specified poses.
    /// </summary>
    public static void ComputePenetration(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance)
        => SharedStaticFunctionPointer.Data
            .Invoke(colliderA, colliderB, positionA, positionB, rotationA, rotationB, out direction, out distance);

    // initialization method that sets up the function pointer
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
    static void AfterAssembliesLoaded()
        => SharedStaticFunctionPointer.Data
            = new FunctionPointer<ComputePenetrationDelegate>(
                ptr: Marshal.GetFunctionPointerForDelegate((ComputePenetrationDelegate) ComputePenetrationManagedImpl)
            );

    // the managed method we're invoking through the function pointer
    // you can use all managed C# features here
    [AOT.MonoPInvokeCallback(typeof(ComputePenetrationDelegate))]
    static void ComputePenetrationManagedImpl(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance)
        => Physics.ComputePenetration(
            colliderA: UnityEngineObjectUtility.FindObjectFromInstanceID<Collider>(colliderA),
            colliderB: UnityEngineObjectUtility.FindObjectFromInstanceID<Collider>(colliderB),
            positionA: positionA,
            positionB: positionB,
            rotationA: rotationA,
            rotationB: rotationB,
            direction: out direction,
            distance: out distance
        );
}

Putting it all together

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using Unity.Burst;
using UnityEngine;

static class UnityEngineObjectUtility
{
    // delegate that lets us invoke an internal method in UnityEngine.Object
    static readonly Func<int, UnityEngine.Object> findObjectFromInstanceId
        = (Func<int, UnityEngine.Object>)
        typeof(UnityEngine.Object)
            .GetMethod("FindObjectFromInstanceID", BindingFlags.NonPublic | BindingFlags.Static)
            .CreateDelegate(typeof(Func<int, UnityEngine.Object>));

    /// <summary> Get object instance based on its instance ID. See also: <see cref="UnityEngine.Object.GetInstanceID"/> </summary>
    public static TObject FindObjectFromInstanceID<TObject>(int instanceId) where TObject : UnityEngine.Object
        => findObjectFromInstanceId.Invoke(instanceId) as TObject;
}

sealed class ComputePenetrationBurstCompatible
{
    // the function pointer, stored in a SharedStatic to make it accessible from Burst
    static readonly SharedStatic<FunctionPointer<ComputePenetrationDelegate>> SharedStaticFunctionPointer
        = SharedStatic<FunctionPointer<ComputePenetrationDelegate>>.GetOrCreate<ComputePenetrationBurstCompatible>();

    // delegate type of the function pointer
    delegate void ComputePenetrationDelegate(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance
    );

    /// <summary>
    /// Burst-compatible wrapper for <see cref="Physics.ComputePenetration"/>.
    /// Computes the minimal translation required to separate the given colliders apart at specified poses.
    /// </summary>
    /// <param name="colliderA">Instance ID of the first collider.</param>
    /// <param name="colliderB">Instance ID of the second collider.</param>
    /// <param name="positionA">Position of the first collider.</param>
    /// <param name="positionB">Position of the second collider.</param>
    /// <param name="rotationA">Rotation of the first collider</param>
    /// <param name="rotationB">Rotation of the second collider.</param>
    /// <param name="direction">Direction along which the translation required to separate the colliders apart is minimal.</param>
    /// <param name="distance">The distance along direction that is required to separate the colliders apart.</param>
    public static void ComputePenetration(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance)
        => SharedStaticFunctionPointer.Data
            .Invoke(colliderA, colliderB, positionA, positionB, rotationA, rotationB, out direction, out distance);

    // initialization method that sets up the function pointer
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
    static void AfterAssembliesLoaded()
        => SharedStaticFunctionPointer.Data
            = new FunctionPointer<ComputePenetrationDelegate>(
                ptr: Marshal.GetFunctionPointerForDelegate((ComputePenetrationDelegate) ComputePenetrationManagedImpl)
            );

    // the managed method we're invoking through the function pointer
    // (you can use good old C# here)
    [AOT.MonoPInvokeCallback(typeof(ComputePenetrationDelegate))]
    static void ComputePenetrationManagedImpl(
        int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance)
        => Physics.ComputePenetration(
            colliderA: UnityEngineObjectUtility.FindObjectFromInstanceID<Collider>(colliderA),
            colliderB: UnityEngineObjectUtility.FindObjectFromInstanceID<Collider>(colliderB),
            positionA: positionA,
            positionB: positionB,
            rotationA: rotationA,
            rotationB: rotationB,
            direction: out direction,
            distance: out distance
        );
}

[BurstCompile]
sealed class UsingManagedFunctionsInBurstCompiledCodeExample : MonoBehaviour
{
    public Collider ColliderA;
    public Collider ColliderB;

    public Vector3 Direction;
    public float Distance;

    void Update()
    {
        // call the example burst-compiled method
        ComputePenetrationUsageExample(
            colliderA: ColliderA.GetInstanceID(),
            colliderB: ColliderB.GetInstanceID(),
            positionA: ColliderA.transform.position,
            positionB: ColliderB.transform.position,
            rotationA: ColliderA.transform.rotation,
            rotationB: ColliderB.transform.rotation,
            direction: out Direction,
            distance: out Distance
        );
    }

    [BurstCompile]
    static void ComputePenetrationUsageExample(int colliderA,
        int colliderB,
        in Vector3 positionA,
        in Vector3 positionB,
        in Quaternion rotationA,
        in Quaternion rotationB,
        out Vector3 direction,
        out float distance)
    {
        // this method is compiled by burst
        // normally we wouldn't be able to use managed code here, but this wrapped method works:
        ComputePenetrationBurstCompatible
            .ComputePenetration(colliderA, colliderB, positionA, positionB, rotationA, rotationB, out direction, out distance);
    }
}
  1. Unfortunately (but probably for a good reason), jobs running on the main thread (eg. using job.Run()) are also forbidden from using Unity APIs, just like a worker thread would be. return ︿

  2. Anything that is a C# class can’t be used with Burst (at least not without a workaround). Sadly, this includes all GameObjects, ScriptableObjects, components… return ︿