I’m trying to solve a bug in my Unity application that I think is caused by using GC handles incorrectly to pin managed (userData) objects. I’m using the Core API directly and might be doing something wrong, but the available documentation differs about some core details.
I found two Unity C# examples on how to set user data in the documentation:
In the timeline example, the GCHandle is pinned and the TimelineInfo class has the [StructLayout(LayoutKind.Sequential)] attribute set, while the DSP capture example has neither. It seems to me that the handle should also be pinned in the DSP capture example, since otherwise the object could be moved by the GC without the unmanaged pointer being updated.
I’m also not entirely clear on what a userdata object should contain. Currently I’m just using it to store a reference to a class instance, and then calling a function on that instance from the static callback, like so:
[StructLayout(LayoutKind.Sequential)]
private class DSPUserData
{
public SomeClass Instance { get; }
public DSPUserData(SomeClass instance)
{
Instance = instance;
}
}
[AOT.MonoPInvokeCallback(typeof(FMOD.DSP_READCALLBACK))]
private static FMOD.RESULT ReadCallback(ref FMOD.DSP_STATE dspState, IntPtr inBuffer, IntPtr outBuffer, uint length, int inChannels, ref int outchannels)
{
FMOD.DSP_STATE_FUNCTIONS functions = (FMOD.DSP_STATE_FUNCTIONS)Marshal.PtrToStructure(dspState.functions, typeof(FMOD.DSP_STATE_FUNCTIONS));
IntPtr userDataPtr;
FMOD.RESULT result = functions.getuserdata(ref dspState, out userDataPtr);
if (result == FMOD.RESULT.OK && userDataPtr != IntPtr.Zero)
{
GCHandle gcHandle = GCHandle.FromIntPtr(userDataPtr);
DSPUserData userData = (DSPUserData)gcHandle.Target;
userData.Instance.SomeFunction();
}
return FMOD.RESULT.ERR_DSP_SILENCE;
}
So my specific questions are:
Should managed objects set as userdata for a system or DSP callback always be pinned and be blittable?
Can the managed userdata object contain a reference to an instance of an object that is then used to call a function in the callback?
If I’m not targeting IL2CPP, can I safely set a class instance function as a callback instead of a static function?
Pinning should only be required when the native code is actually touching/modifying the data being passed in, and userdata is only passed through the native FMOD code to the callback.
The ‘Timeline Callbacks’ example should probably be updated to have to the pinning and structLayout removed.
The userdata can be anything, you can customize it to pass through whatever information you may need.
Managed methods that need to be marshaled to a C function pointer so that they can be called from native code have a few restrictions on AOT platforms:
The managed method must be a static method
The managed method must have the [MonoPInvokeCallback] attribute
I’ve never been able to reproduce the bug locally, but several users have reported it. After about 45 ~ 60 minutes of using the application, all audio playback stops, and also some of the VR input controls (ex: buttons, grabbing objects, etc…) seem to randomly stop working.
The fact that it only happens after a certain period and seems to affect the app in unpredictable and random ways, makes me suspect it’s a memory corruption issue. One of my theories was that it might be caused by the unmanaged FMOD code using an outdated pointer to a managed object (ex: callback, userData) that had been moved by the GC.
I don’t have any crash dumps, as the application itself actually does not crash in the instances that have been reported by users, but I have received some crash reports related to FMOD through cloud diagnostics which might be related.
Here’s the most recent example (changed the extension from .json to .txt so I could upload it): NvExg3gBVouaglDsmPUy_data.txt (351.9 KB)
Problem
Native Crash - Unknown Function (fmodstudio)
One thing that I noticed I did differently than in the DSP capture example was how I called the getUserData function in the DSP’s read callback.
In the previous version of my app (which the report I posted above is from) I used this code:
[AOT.MonoPInvokeCallback(typeof(FMOD.DSP_READCALLBACK))]
private static FMOD.RESULT ReadCallback(ref FMOD.DSP_STATE dspState, IntPtr inBuffer, IntPtr outBuffer, uint length, int inChannels, ref int outchannels)
{
FMOD.DSP dsp = new FMOD.DSP();
dsp.handle = dspState.instance;
IntPtr userDataPtr;
FMOD.RESULT result = dsp.getUserData(out userDataPtr);
.......
}
In the latest version I’ve now changed it to match the DSP capture example:
FMOD.DSP_STATE_FUNCTIONS functions = (FMOD.DSP_STATE_FUNCTIONS)Marshal.PtrToStructure(dspState.functions, typeof(FMOD.DSP_STATE_FUNCTIONS));
FMOD.RESULT result = functions.getuserdata(ref dspState, out userDataPtr);
Could my previous way of calling the getUserData function cause these type of issues?
Both of these methods should work and do the same thing. Although it isn’t recommended to use the first method as you should not be using the public API in a plugin, plugins are usually meant to be a standalone and therefore cannot reference the public API, only the plugin API, but in this case it should be fine to do.
As for the logs, unfortunately there are no symbols so we cannot begin to speculate at the cause. Are you able to try reproducing the issue while using the FMOD logging libs, this will provide much more information.