I have 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); }
This calls function C ,
[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_AddEventWatch")] internal static extern void SDL_AddEventWatch(SDL_EventFilter filter, IntPtr userData);
which is expecting 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 great as it is. My understanding is that a lambda expression can be cleared by the CLR garbage collector or moved to memory, because it does not know that the DLL holds a reference to it.
- It's true?
- If so, I understand that the
fixed keyword is used to prevent such a move,- How to apply
fixed to a delegate? - Even if I fix it, will it still not be cleaned / deleted because it is out of scope?
I experimented a bit. I called GC.Collect(); immediately after adding an event, but before it is triggered. It throws a CallbackOnCollectedDelegate exception, which is actually much nicer than the expected hard crash.
Darin's solution works, but the Marshal.GetFunctionPointerForDelegate step is unnecessary. The C SDL_EventFilter will take SDL_EventFilter just fine, there is no need to do it IntPtr . Additionally, creating IntPtr via GCHandle.ToIntPtr(gch) actually causes a crash when the event GCHandle.ToIntPtr(gch) . Not sure why; the method seems to be built for this, and it is even used in the MSDN example .
An article in which Darin refers to conditions:
Note that the [descriptor] does not need to be committed to any specific memory location. Therefore, the version of GCHandle.Alloc () that accepts the GCHandleType parameter:
GCHandle gch = GCHandle.Alloc(callback_delegate, GCHandleType.Pinned);
no need to use.
However, MSDN doesn't say anything about GCHandleType.Normal , which prevents callback movement. In fact, the .Pinned description reads:
This prevents the object garbage collector from moving.
So I tried it. It throws an ArgumentException using the help text:
The object contains non-primitive or non-reproducible data.
I can only hope that the article does not lie that it does not need to be riveted because I do not know how to test this scenario.
At the moment, 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, simply adding the ef dictionary to the dictionary prevents garbage collection. I'm not sure if GCHandle.Alloc doing anything besides this.