Callback delegates being collected?

2019-01-19 10:51发布

问题:

Been messing around with FMOD for C# game development and I've hit a snag early on that I can't seem to get around. I want to do some branching audio stuff and sync some gameplay action to beats and such, so I've tried adding syncpoints to my music tracks. Here's code:

public class Music
{
    private Sound music;
    private Channel channel;
    private IntPtr syncPtr;

    public string File { get; private set; }  

    public Music(string file)
    {
        File = file;
    }

    public void Load()
    {
        music = new Sound();
        Audio.System.createSound(File, MODE.HARDWARE, ref music);
    }

    public void Unload()
    {
        music.release();
    }

    public virtual void Play()
    {
        Audio.System.playSound(channel == null ? CHANNELINDEX.FREE : CHANNELINDEX.REUSE, music, false, ref channel);
        music.addSyncPoint(500, TIMEUNIT.MS, "wooo", ref syncPtr);
        channel.setCallback(channelCallback);
    }

    private RESULT channelCallback(IntPtr channelraw, CHANNEL_CALLBACKTYPE type, IntPtr commanddata1, IntPtr commanddata2)
    {
        if (type == CHANNEL_CALLBACKTYPE.SYNCPOINT)
            Console.WriteLine("sync!");

        return RESULT.OK;
    }
}

And then...

m = new Music(MUS_TUTORIAL);  //m is static
m.Load();
m.Play();

The song loads and plays fine... until it hits that 500ms syncpoint I added. At this point, VC# spits out the following error from within FMOD.EventSystem.update():

A callback was made on a garbage collected delegate of type 'Game!FMOD.CHANNEL_CALLBACK::Invoke'. This may cause application crashes, corruption and data loss. When passing delegates to unmanaged code, they must be kept alive by the managed application until it is guaranteed that they will never be called.

So somehow FMOD is losing track of the delegate I passed it. The Music instance that holds the delegate has not been garbage collected - I'm storing it in a static variable for now - but I've tried with a static method too to no avail. If I disable the CallbackOnCollectedDelegate MDA the error becomes a null reference exception, so the MDA isn't mistaken. I assume I must just not fully understand what FMOD is doing here.

Are any C# + FMOD gurus able to see my mistake?

回答1:

    channel.setCallback(channelCallback);

That's the problem statement. FMod is unmanaged code. You are creating a delegate object here and passing it to the unmanaged code. Problem is, the garbage collector cannot track references held by native code. The next garbage collection will find no references to the object and collect it. Kaboom when the native code makes the callback.

You need to keep a reference yourself so this won't happen:

public class Music
{
    private SomeDelegateType callback
    //...
    public Music(string file)
    {
        File = file;
        callback = new SomeDelegateType(channelCallback);
    }

    public virtual void Play()
    {
        Audio.System.playSound(channel == null ? CHANNELINDEX.FREE : CHANNELINDEX.REUSE, music, false, ref channel);
        music.addSyncPoint(500, TIMEUNIT.MS, "wooo", ref syncPtr);
        channel.setCallback(callback);
    }

You need to find the actual delegate type from the FMod wrapper code, I merely guessed at "SomeDelegateType".



回答2:

I had a similar problem between VB.NET and a custom C++ DLL. Fixed thanks to @Hans. I owe this site many times over for all the problems it's got me through. Adding my problem+solution in hopes it helps others seeing the same solution in a different context.

Declared this (in a module)

Public Delegate Sub CB_FUNC(ByVal x As Integer, ByVal y As Integer)
Public Declare Sub vidProc_cb_MouseClick Lib "C:\Users\.....\vidProc\product\vidProc.dll" (ByVal addr_update As CB_FUNC)

Originally:

Had a simple call in a button_click sub:

vidProc_cb_MouseClick(AddressOf updateXY)

I would get the 'CallbackOnCollectedDelegate' error. Not immediately, but after interacting with other objects on the form, then trying to invoke the callback (which in my case was a mouse click in an OpenCV window).

Fixed by:

1) Declaring (in the Form Class, Declarations)

Private addr_update As CB_FUNC

2) Defining addr_update on Form Load

addr_update = New CB_FUNC(AddressOf updateXY)

3) Calling my 'set callback' function with the new pointer (in the button_click sub)

vidProc_cb_MouseClick(addr_update)

I think I understood @Hans and implemented it correctly (I can't reproduce the error). Hope this helps someone.