mpen
mpen

Reputation: 283313

How to "fix" a lambda expression?

I've got a method,

public static void AddEventWatch(EventFilter filter)
{
    SDL_AddEventWatch((IntPtr data, ref SDL_Event e) =>
    {
        filter(new Event(ref e));
        return 0;
    }, IntPtr.Zero);
}

That calls a C function,

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_AddEventWatch")]
internal static extern void SDL_AddEventWatch(SDL_EventFilter filter, IntPtr userData);

which expects a callback.

As shown above, I pass the SDL_EventFilter in the form of a lambda expression, which is later called by the C API.

Through preliminary testing, this works perfectly fine as-is. My understanding though is that the lambda expression can be cleaned up by the CLR garbage collector or moved around in memory, since it has no knowledge that the DLL is holding a reference to it.

  1. Is this true?
  2. If so, I understand that the fixed keyword is used to prevent such moving,
    1. How do I apply fixed to a delegate?
    2. Even if I 'fix' it, would it not still be cleaned up/deleted because it's gone out of scope?

I did some experimentation. I called GC.Collect(); right after adding an event, but before triggering it. It throws a CallbackOnCollectedDelegate exception, which is actually much more pleasant than the hard crash I was expecting.

Darin's solution does appear to work, but the Marshal.GetFunctionPointerForDelegate step appears to be unnecessary. The C callback will take a SDL_EventFilter just fine, there's no need to make it an IntPtr. Further, creating the IntPtr via GCHandle.ToIntPtr(gch) actually causes a crash when the event is fired. Not sure why; it appears the method is built for that and it's even used in the MSDN example.

The article that Darin links to states:

Note that [the handle] need not be fixed at any specific memory location. Hence the version of GCHandle.Alloc() that takes a GCHandleType parameter :

GCHandle gch = GCHandle.Alloc(callback_delegate, GCHandleType.Pinned);

need not be used.

However, MSDN doesn't say anything about GCHandleType.Normal preventing the callback from being moved. In fact, the description of .Pinned reads:

This prevents the garbage collector from moving the object

So I tried it. It causes a ArgumentException with the help text:

Object contains non-primitive or non-blittable data

I can only hope the article is not lying about it not needing to be pinned, because I don't know how to test this scenario.


For now, this is the solution I'm working with:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int SDL_EventFilter(IntPtr userData, ref SDL_Event @event);

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_AddEventWatch")]
internal static extern void SDL_AddEventWatch(SDL_EventFilter filter, IntPtr userData);

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_DelEventWatch")]
internal static extern void SDL_DelEventWatch(SDL_EventFilter filter, IntPtr userData);

public delegate void EventFilter(Event @event);
private static readonly Dictionary<EventFilter, Tuple<SDL_EventFilter, GCHandle>> _eventWatchers = new Dictionary<EventFilter, Tuple<SDL_EventFilter, GCHandle>>(); 

public static void AddEventWatch(EventFilter filter)
{
    SDL_EventFilter ef = (IntPtr data, ref SDL_Event e) =>
    {
        filter(new Event(ref e));
        return 0;
    };
    var gch = GCHandle.Alloc(ef);
    _eventWatchers.Add(filter, Tuple.Create(ef,gch));
    SDL_AddEventWatch(ef, IntPtr.Zero);
}

public static void DelEventWatch(EventFilter filter)
{
    var tup = _eventWatchers[filter];
    _eventWatchers.Remove(filter);
    SDL_DelEventWatch(tup.Item1, IntPtr.Zero);
    tup.Item2.Free();
}

However, merely adding adding ef to the dictionary prevents garbage collection. I'm not really sure if GCHandle.Alloc does anything beyond that.

Upvotes: 4

Views: 524

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1039468

1) Is this true?

Yes.

2) How do I apply fixed to a delegate?

Define your method signature like this:

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_AddEventWatch")]
internal static extern void SDL_AddEventWatch(IntPtr filterPointer, IntPtr userData);

and then:

public static void AddEventWatch(EventFilter filter)
{
    SDL_EventFilter myFilter = (IntPtr data, ref SDL_Event e) => 
    {
        filter(new Event(ref e));
        return 0;    
    };

    GCHandle gch = GCHandle.Alloc(myFilter);
    try
    {
        var filterPointer = Marshal.GetFunctionPointerForDelegate(myFilter);
        SDL_AddEventWatch(filterPointer, IntPtr.Zero);
    }
    finally
    {
        gch.Free();
    }
}

Basically as long as you are holding the GCHandle in memory, the callback will not be moved around or GCed.

The following article goes in more details: http://limbioliong.wordpress.com/2011/06/19/delegates-as-callbacks-part-2/

Upvotes: 3

Related Questions