Quick answer: Decorate the static callback with [MonoPInvokeCallback(typeof(NativeDelegate))]. From inside, capture the main thread SynchronizationContext at startup; _main.Post(_ => {...}, null) to do UnityEngine work safely.
Native plugin fires a download-complete callback. Your Unity code tries to update a Text component. Crash — the callback ran on a worker thread.
The Symptom
IL2CPP build crashes inside an UnityEngine API call from a P/Invoke callback. Editor (Mono) often forgives; IL2CPP doesn’t.
The Fix
using AOT;
using System.Threading;
public class NativeBridge : MonoBehaviour
{
private static SynchronizationContext _main;
void Awake() { _main = SynchronizationContext.Current; }
[MonoPInvokeCallback(typeof(NativeCallback))]
private static void OnDownloaded(IntPtr data, int len)
{
var bytes = new byte[len];
Marshal.Copy(data, bytes, 0, len);
_main.Post(_ =>
{
// Safe: this runs on the main thread next loop
UpdateUI(bytes);
}, null);
}
}
MonoPInvokeCallback ensures IL2CPP keeps the trampoline. Post marshals to the main thread.
Static-Only
P/Invoke callback handlers must be static (or a delegate referring to a static). Use a static dispatch with a stable instance reference passed by IntPtr if you need the instance.
Verifying
IL2CPP build. Trigger native callback. UI updates without crash. Without Post: crash inside UnityEngine.UI on touch.
“MonoPInvokeCallback. Capture context. Post to main. Native plays nice.”
Related Issues
For IL2CPP strip reflection, see strip reflection. For async unobserved, see async exceptions.
Static callback. Marshal back. Main thread runs.