I just recently learned about SafeHandle and, for a test, I implemented it for the SDL2 library, creating and destroying a window:
[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr SDL_CreateWindow(
[MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);
[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void SDL_DestroyWindow(IntPtr window);
public class Window : SafeHandleZeroOrMinusOneIsInvalid
{
public Window() : base(true)
{
SetHandle(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0));
}
protected override bool ReleaseHandle()
{
SDL_DestroyWindow(handle);
return true;
}
}
This works fine, then I learned of another advantage of using SafeHandle: The possibility to use the class in the p/invoke signature directly, like so:
[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern Window SDL_CreateWindow(
[MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);
[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void SDL_DestroyWindow(Window window);
This is of course much better than generic IntPtr parameters / returns, because I have type safety passing / retrieving actual Window (handles) to / from those methods.
While this works for SDL_CreateWindow, which correctly returns a Window instance now, it doesn't work for SDL_DestroyWindow, which is called by me inside of Window.ReleaseHandle like this:
public Window() : base(true)
{
SetHandle(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0).handle);
}
protected override bool ReleaseHandle()
{
SDL_DestroyWindow(this);
return true;
}
When trying to pass this to SDL_DestroyWindow, I get an ObjectDisposedException: Safe handle has been closed. Indeed the IsClosed property is true, which I didn't expect to be at this time. Apparently it internally tries to increase a reference count, but notices IsClosed is true. According to the documentation, it has been set to true because "The Dispose method or Close method was called and there are no references to the SafeHandle object on other threads.", so I guess Dispose was implicitly called before in the call stack to invoke my ReleaseHandle.
ReleaseHandle apparently isn't the correct place to clean up if I want to use a class parameter in the p/invoke signature, so I wonder if there is any method where I could clean up without breaking SafeHandle internals?
My question above is slightly misguided by wrong information I learned about
SafeHandle(via some blog posts I won't mention). While I was told that replacingIntPtrparameters in P/Invoke methods with class instances is "the main advantage provided bySafeHandle" and definitely nice, it turns out to be only partly useful:Careful with automatic
SafeHandlecreation by the marshallerFor one, I say this because my code above has a big issue I didn't see at first. I have written this code:
When the marshaller calls the P/Invoke method of
SDL_CreateWindowin theWindowconstructor, it internally creates another instance of theWindowclass for the return value (calling the required parameterless constructor and then setting thehandlemember internally). This means I now have two SafeHandle instances:SDL_CreateWindowmethod - which I don't use anywhere (only stripping out thehandleproperty)new Window(), theSafeHandleclass itselfThe only correct way to implement the
SafeHandlehere is to letSDL_CreateWindowreturn anIntPtragain, so no internal marshallingSafeHandleinstances are created anymore.Can't pass
SafeHandlearound inReleaseHandleAs Simon Mourier explained / cited in the comments, the
SafeHandleitself cannot be used at all anymore when cleaning up inReleaseHandle, since the object is garbage collected and trying to do "fancy" things like passing it to a P/Invoke method is not safe anymore / doomed to fail. (Given I were told that the replacement ofIntPtrparameters in the P/Invoke is one of "the main features" ofSafeHandle, it surprised me first that this is not supported and deemed "fancy"). That is also why theObjectDisposedExceptionI receive is very justified.I can still access the
handleproperty here, but then, again, my P/Invoke method no longer accepts aWindowinstance, but the "classic"IntPtr.Am I better off with IntPtr for P/invoke parameters again?
I'd say so, my final implementation looks like this and solves the above two problems while still using the advantages of
SafeHandle, just without the fancy P/Invoke argument replacements. And as an extra feature, I can still connote the IntPtr parameters to "accept" an SDL_Window (the native type pointed to) with ausingalias.