Reputation: 283313
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.
fixed
keyword is used to prevent such moving,
fixed
to a delegate?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
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