SafeHandles provide reliable and secure means of managing the lifetime of unmanaged resources. It’s commonly recommended to use SafeHandle
instead of IntPtr
whenever we need to acquire and deal with unmanaged system handles.
The .NET runtime makes this recommendation easy to follow, as the SafeHandle
class is integrated with platform invoke, allowing for the correct type of SafeHandle
to be used interchangeably with IntPtr
as parameters in P/Invoke method signatures.
Unmanaged handles aren’t only used as method parameters, however, as they are also present as members of the many kinds of C-style structs that unmanaged functions accept as parameters. Unfortunately, with structs, the use of SafeHandle
is not interchangeable with IntPtr
.
This means that we’re still stuck using dangerous IntPtr
handles when dealing with structs. However, with .NET 7’s introduction of source generation for platform invokes, we now have a way to safely make use of SafeHandle
types in our structs. This article will demonstrate how.
A Sample Handle
We’re going to take a look at an example SafeHandle
and its current limitations, and then look at how we can overcome them as far structs are concerned.
One kind of unmanaged resource we encounter frequently is a handle to a window. It is created when a window is created, and it is used to interact with said window throughout its lifetime.
WindowHandle.cs
/// <summary> /// Provides a level-0 type for window handles. /// </summary> /// <suppressions> /// ReSharper disable UnusedMember.Local /// </suppressions> public sealed class WindowHandle : SafeHandle { /// <summary> /// Initializes a new instance of the <see cref="WindowHandle"/> class. /// </summary> /// <param name="handle">The handle to the window.</param> /// <param name="ownsHandle"> /// Value indicating if this safe handle is responsible for releasing the provided handle. /// </param> public WindowHandle(IntPtr handle, bool ownsHandle) : this(ownsHandle) { SetHandle(handle); } /// <summary> /// Initializes a new instance of the <see cref="WindowHandle"/> class. /// </summary> public WindowHandle() : this(true) { } /// <summary> /// Initializes a new instance of the <see cref="WindowHandle"/> class. /// </summary> /// <param name="ownsHandle"> /// Value indicating if this safe handle is responsible for releasing the provided handle. /// </param> private WindowHandle(bool ownsHandle) : base(IntPtr.Zero, ownsHandle) { } /// <summary> /// Gets a default invalid instance of the <see cref="WindowHandle"/> class. /// </summary> public static WindowHandle InvalidHandle => new(false); /// <inheritdoc/> public override bool IsInvalid => handle == IntPtr.Zero; /// <inheritdoc/> protected override bool ReleaseHandle() => User32.DestroyWindow(handle); }
The WindowHandle
class provides SafeHandle
-like functoinality for unmanaged window handles.
It’ll be initialized via the default constructor by P/Invoke whenever it’s used as a return value for a function that normally returns a window handle.
P/Invoke Function Returning WindowHandle
[LibraryImport(LIBRARY_NAME, EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] public static unsafe partial WindowHandle CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int style, int x, int y, int width, int height, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, void* lpParam);
Normally, the signature for CreateWindowEx
would be written to return an IntPtr
handle to the window. But, as you can see, we can easily swap that with our custom SafeHandle
and it all works like magic.
Our custom SafeHandle
can also be used as a parameter for methods that expect a window handle and are normally written to accept an IntPtr
.
P/Invoke Function Accepting WindowHandle as Parameter
[LibraryImport(LIBRARY_NAME, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool GetWindowRect(WindowHandle hWnd, out RECT lpRect);
And everything works just perfectly, with the added security and reliability that using a SafeHandle
grants us.
Unfortunately, that’s where the magic just about stops.
Structs and SafeHandles Don’t Mix Well
If we continue to expand our repertoire of window-related P/Invoke functions, we’ll eventually run into some methods expecting window handles to be provided as members of a C-style struct.
An example is the Shell_NotifyIcon
function, used to register an icon with the taskbar’s status area. This function accepts a NOTIFYICONDATA
struct as one of its parameters, which has a member that gets set to the handle of the window meant to receive taskbar notifications.
If we try to define this structure so that it uses a WindowHandle
, we will run into problems.
A Problematic NOTIFYICONDATA
/// <summary> /// Represents information that the system needs to display notifications in the notification area. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal unsafe struct NOTIFYICONDATAW { /// <summary> /// A handle to the window that receives notifications associated with an icon i /// n the notification area. /// </summary> public WindowHandle hWnd; // Insert the rest of the members.. }
The presence of a WindowHandle
in this struct will immediately result in compile-time errors similar to the following if we try to use the struct as a parameter to Shell_NotifyIcon
:
SYSLIB10: The type 'BadEcho.Interop.NOTIFYICONDATAW' is not supported by source-generated P/Invokes.
The generated source will not handle marshalling of parameter 'lpData'.
This is an error you’d get when using the new source generator system à la the LibraryImportAttribute
. If you’re using the older DllImportAttribute
, then your program may compile, however it isn’t going to work at runtime.
So what can we do? Well, let’s start with the more obvious, less-than-perfect, workaround.
The Bad Way: Getting a Handle Out of a SafeHandle
We can revert our struct definition to use IntPtr
instead of our WindowHandle
. This will resolve the compile-time errors.
All we need to do then is squeeze the SafeHandle
‘s internal handle out from it, and use that when populating the struct’s members.
A Dangerous Squeeze
var iconData = new NOTIFYICONDATAW { cbSize = (uint) sizeof(NOTIFYICONDATAW), uCallbackMessage = (uint) _TrayEvent, uFlags = NotifyIconFlags.Message | NotifyIconFlags.Guid, guidItem = _id, hWnd = windowHandle.DangerousGetHandle() // Everything else... }; Shell32.Shell_NotifyIcon(NotifyIconMessage.Add, ref iconData);
Getting the internal handle out from the SafeHandle
requires the calling of DangerousGetHandle
, a method with an appropriately ominous name.
There are numerous articles out there which go into why using this method is, for lack of a better word, “dangerous”. It opens ourselves up to runtime errors due to stale handles, as well as potential security issues via handle recycling security attacks.
To use this method safely, we need to use the DangerousAddRef
and DangerousRelease
methods — and the best place to use these is right in the middle of the whole marshalling procedure, somewhere that’s been out of reach to us until .NET 7’s release.
This leads us to the better way to “handle” our SafeHandle
-in-structs conundrum: by implementing a custom stateful marshaller for this struct.
The Good Way: A Stateful Marshaller
We’re going to leverage some of .NET 7’s new P/Invoke source generation technology so that we can keep our WindowHandle
, and a few other SafeHandle
instances that we require, where we want them in the call to Shell_NotifyIcon
.
A custom marshaller allows for fine-grained control over how a type is marshalled (transformed when crossing between managed and native code).
Most (this is a bit of an assumption) marshallers are ‘stateless’, meaning they are static classes that maintain no state regarding the object being marshalled. You can see an example of one here.
Custom Marshaller Components
We’ll be making a marshaller that keeps track of the object and its constituent parts during the marshalling process. Maintaining state is required so that we can correctly update the reference counters on our SafeHandle
instances.
Custom marshalling requires the following components:
- A managed data type
- An unmanaged data type
- The custom marshaller type
When we’re done, we’ll be able to define a P/Invoke signature that receives the managed data class in place of the C-style struct, and it will all work fabulously.
The Managed Data Type
The managed data type will be the class our consuming code will be directly creating and interacting with when calling our P/Invoke function. Consuming code will never interact with the unmanaged data type.
Despite how we are referring to it, the “unmanaged” data type, defined using C# code, will be managed code itself. Our managed data type will differ from the “unmanaged” type in that it will contain all the member types not supported by source-generated P/Invokes, such as our WindowHandle
.
Also, while the managed type could (and, in practice, sometimes is) defined as a struct, I tend to define them as classes; this is because, being purely managed representations of the data, I find it more appropriate to define them as how one defines the majority of types which don’t require value type semantics in a .NET assembly: as classes with properties.
NotifyIconData.cs
/// <summary> /// Provides information that the system needs to display notifications in the /// notification area. /// </summary> [NativeMarshalling(typeof(NotifyIconDataMarshaller))] internal sealed class NotifyIconData { /// <summary> /// Initializes a new instance of the <see cref="NotifyIconData"/> class. /// </summary> /// <param name="window"> /// A handle to the window that receives notifications associated with an icon in the /// notification area. /// </param> /// <param name="id">The unique identifier of the taskbar icon.</param> /// <param name="flags"> /// Flags that either indicate which of the other members of the structure contain valid /// data or provide additional information to the tooltip as to how it should display. /// </param> public NotifyIconData(WindowHandle window, Guid id, NotifyIconFlags flags) { Require.NotNull(window, nameof(window)); Window = window; Id = id; // It is implied that the Guid identifier member is valid given our constructor's // parameters. Flags = flags | NotifyIconFlags.Guid; } /// <summary> /// Gets a handle to the window that receives notifications associated with an icon /// in the notification area. /// </summary> public WindowHandle Window { get; } /// <summary> /// Gets the unique identifier of the taskbar icon. /// </summary> public Guid Id { get; } /// <summary> /// Gets flags that either indicate which of the other members of the structure contain /// valid data or provide additional information to the tooltip as to how it should display. /// </summary> public NotifyIconFlags Flags { get; } /// <summary> /// Gets or sets an application-defined message identifier. /// </summary> public uint CallbackMessage { get; set; } /// <summary> /// Gets or sets a handle to the icon to be added, modified, or deleted. /// </summary> public IconHandle? Icon { get; set; } /// <summary> /// Gets or sets the state of the icon. /// </summary> public uint State { get; set; } /// <summary> /// Gets or sets a value that specifies which bits of the <see cref="State"/> member /// are retrieved or modified. /// </summary> public uint StateMask { get; set; } /// <summary> /// Gets or sets either the timeout value, in milliseconds, for the notification, or a /// specification of which version of the Shell notification icon interface should be used. /// </summary> public uint TimeoutOrVersion { get; set; } /// <summary> /// Gets or sets Flags that can be set to modify the behavior and appearance of a /// balloon notification. /// </summary> public NotifyIconInfoFlags InfoFlags { get; set; } /// <summary> /// Gets or sets A handle to a customized notification icon that should be used independently /// of the notification area icon. /// </summary> public IconHandle? BalloonIcon { get; set; } /// <summary> /// Gets or sets a string that specifies the text for a standard tooltip. /// </summary> public string? Tip { get; set; } /// <summary> /// Gets or sets a string that specifies the text to display in a balloon notification. /// </summary> public string? Info { get; set; } /// <summary> /// Gets or sets a string that specifies a title for a balloon notification. /// </summary> public string? InfoTitle { get; set; } }
We see above the managed data representation of the NOTIFYICONDATA
struct; it is a pretty typical looking POCO.
One thing a bit out of the ordinary is the NativeMarshallingAttribute
at the top of the class. The presence of this attribute will cause the specified marshaller to be automatically used when this managed type is present in a P/Invoke signature.
The marshaller type specified, NotifyIconDataMarshaller
, is our custom marshaller which we’ll be defining later.
The Unmanaged Data Type
The next step is to define the unmanaged data type. As was mentioned earlier, this “unmanaged” data type’s code will be as managed as everything else we’re writing.
What will make it “unmanaged” is that it will be a C-style struct that contains only member types supported by source-generated P/Invokes.
That means every member must be a built-in value type. So, no SafeHandle
types are allowed here; instead, IntPtr
must be used for handles. Delegate types will need to be replaced with function pointers, strings will need to be replaced with ushort
or byte
pointers, etc.
Proper struct authoring is outside the scope of this article; suffice it to say: the unmanaged data type will be the struct we would’ve been directly feeding to the P/Invoke function if we weren’t planning on doing any custom marshalling.
NOTIFYICONDATAW Struct
/// <summary> /// Represents information that the system needs to display notifications in the notification area. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct NOTIFYICONDATAW { /// <summary> /// The size of this structure in bytes. /// </summary> public uint cbSize; /// <summary> /// A handle to the window that receives notifications associated with an icon in /// the notification area. /// </summary> public IntPtr hWnd; /// <summary> /// The application-defined identifier of the taskbar icon. /// </summary> public uint uID; /// <summary> /// Flags that either indicate which of the other members of the structure contain valid data /// or provide additional information to the tooltip as to how it should display. /// </summary> public NotifyIconFlags uFlags; /// <summary> /// An application-defined message identifier. /// </summary> public uint uCallbackMessage; /// <summary> /// A handle to the icon to be added, modified, or deleted. /// </summary> public IntPtr hIcon; /// <summary> /// A null-terminated string that specifies the text for a standard tooltip. /// </summary> public fixed char szTip[128]; /// <summary> /// The state of the icon. /// </summary> public uint dwState; /// <summary> /// A value that specifies which bits of the <see cref="dwState"/> member are retrieved /// or modified. /// </summary> public uint dwStateMask; /// <summary> /// A null-terminated string that specifies the text to display in a balloon notification. /// </summary> public fixed char szInfo[256]; /// <summary> /// Either the timeout value, in milliseconds, for the notification, or a specification of which /// version of the Shell notification icon interface should be used. /// </summary> public uint uTimeoutOrVersion; /// <summary> /// A null-terminated string that specifies the title for a balloon notification. /// </summary> public fixed char szInfoTitle[64]; /// <summary> /// Flags that can be set to modify the behavior and appearance of a balloon notification. /// </summary> public NotifyIconInfoFlags dwInfoFlags; /// <summary> /// A registered <see cref="GUID"/> that identifies the icon. This is the preferred way to /// identify an icon in modern OS versions. /// </summary> public GUID guidItem; /// <summary> /// A handle to a customized notification icon that should be used independently of the /// notification area icon. /// </summary> public IntPtr hBalloonIcon; /// <summary> /// Gets or sets a string that specifies the text for a standard tooltip. /// </summary> public ReadOnlySpan<char> Tip { get => SzTip.SliceAtFirstNull(); set => value.CopyToAndTerminate(SzTip); } /// <summary> /// Gets or sets a string that specifies the text to display in a balloon notification. /// </summary> public ReadOnlySpan<char> Info { get => SzInfo.SliceAtFirstNull(); set => value.CopyToAndTerminate(SzInfo); } /// <summary> /// Gets or sets a string that specifies a title for a balloon notification. /// </summary> public ReadOnlySpan<char> InfoTitle { get => SzInfoTitle.SliceAtFirstNull(); set => value.CopyToAndTerminate(SzInfoTitle); } /// <summary> /// Gets a null-terminated string that specifies the text for a standard tooltip. /// </summary> private Span<char> SzTip { get { fixed (char* c = szTip) { return new Span<char>(c, 128); } } } /// <summary> /// Gets a null-terminated string that specifies the text to display in a balloon notification. /// </summary> private Span<char> SzInfo { get { fixed (char* c = szInfo) { return new Span<char>(c, 256); } } } /// <summary> /// Gets a null-terminated string that specifies a title for a balloon notification. /// </summary> private Span<char> SzInfoTitle { get { fixed (char* c = szInfoTitle) { return new Span<char>(c, 64); } } } }
The only other thing to add here is that it is common practice to include the unmanaged data type as a nested type inside the custom marshaller (which we will be covering next). This discourages its use outside of the marshaller.
Now, let’s write that marshaller!
Our Custom Stateful Marshaller
On to the main show. We will be writing a custom stateful marshaller that will marshal notification area information containing a number of custom SafeHandle
types.
Most of the time, a custom marshaller will be stateless and consist only of a static class with a few required static methods.
For a stateful marshaller, we need a static class (which will be referenced by the NativeMarshallingAttribute
adorning our managed data type) and then, inside of that, an inner struct that will hold state information and provide the marshalling methods.
NotifyIconDataMarshaller.cs
/// <summary> /// Provides a custom marshaller for notification area information. /// </summary> [CustomMarshaller( typeof(NotifyIconData), MarshalMode.ManagedToUnmanagedRef, typeof(ManagedToUnmanagedRef))] internal static unsafe class NotifyIconDataMarshaller { /// <summary> /// Represents a stateful marshaller for notification area information. /// </summary> public ref struct ManagedToUnmanagedRef { private NOTIFYICONDATAW _unmanaged; private WindowHandle _windowHandle; private IconHandle? _iconHandle; private IconHandle? _balloonIconHandle; private bool _windowHandleAddRefd; private bool _iconHandleAddRefd; private bool _balloonIconHandleAddRefd; private IntPtr _originalWindowHandleValue; private IntPtr _originalIconHandleValue; private IntPtr _originalBalloonIconHandleValue; /// <summary> /// Converts a managed <see cref="NotifyIconData"/> instance to its unmanaged counterpart, /// loading the result into the marshaller. /// </summary> /// <param name="iconData">A managed instance of notification area information.</param> public void FromManaged(NotifyIconData iconData) { Require.NotNull(iconData, nameof(iconData)); _windowHandleAddRefd = false; _iconHandleAddRefd = false; _balloonIconHandleAddRefd = false; _windowHandle = iconData.Window; _iconHandle = iconData.Icon; _balloonIconHandle = iconData.BalloonIcon; _unmanaged.cbSize = (uint) sizeof(NOTIFYICONDATAW); _windowHandle.DangerousAddRef(ref _windowHandleAddRefd); _unmanaged.hWnd = _originalWindowHandleValue = _windowHandle.DangerousGetHandle(); _unmanaged.guidItem = GuidMarshaller.ConvertToUnmanaged(iconData.Id); _unmanaged.uFlags = iconData.Flags; _unmanaged.uCallbackMessage = iconData.CallbackMessage; if (_iconHandle != null) { _iconHandle.DangerousAddRef(ref _iconHandleAddRefd); _unmanaged.hIcon = _originalIconHandleValue = _iconHandle.DangerousGetHandle(); } _unmanaged.dwState = iconData.State; _unmanaged.dwStateMask = iconData.StateMask; _unmanaged.uTimeoutOrVersion = iconData.TimeoutOrVersion; _unmanaged.dwInfoFlags = iconData.InfoFlags; if (_balloonIconHandle != null) { _balloonIconHandle.DangerousAddRef(ref _balloonIconHandleAddRefd); _unmanaged.hBalloonIcon = _originalBalloonIconHandleValue = _balloonIconHandle.DangerousGetHandle(); } _unmanaged.Tip = iconData.Tip; _unmanaged.Info = iconData.Info; _unmanaged.InfoTitle = iconData.InfoTitle; } /// <summary> /// Provides the unmanaged notification area information currently loaded into the /// marshaller. /// </summary> /// <returns>The converted <see cref="NOTIFYICONDATAW"/> value.</returns> public NOTIFYICONDATAW ToUnmanaged() => _unmanaged; /// <summary> /// Loads the provided unmanaged notification area information into the /// marshaller. /// </summary> /// <param name="unmanaged">The unmanaged instance of notification area information.</param> public void FromUnmanaged(NOTIFYICONDATAW unmanaged) => _unmanaged = unmanaged; /// <summary> /// Converts the unmanaged <see cref="NOTIFYICONDATAW"/> instance currently loaded into /// the marshaller into its managed counterpart, returning the result. /// </summary> /// <returns>The converted <see cref="NotifyIconData"/> instance.</returns> /// <exception cref="NotSupportedException"> /// A handle originating from a <see cref="SafeHandle"/> was changed. /// </exception> public NotifyIconData ToManaged() { // SafeHandle fields must match the underlying handle value during marshalling. // They cannot change. if (_unmanaged.hWnd != _originalWindowHandleValue) throw new NotSupportedException(Strings.HandleCannotChangeDuringMarshalling); if (_unmanaged.hIcon != _originalIconHandleValue) throw new NotSupportedException(Strings.HandleCannotChangeDuringMarshalling); if (_unmanaged.hBalloonIcon != _originalBalloonIconHandleValue) throw new NotSupportedException(Strings.HandleCannotChangeDuringMarshalling); Guid managedId = GuidMarshaller.ConvertToManaged(_unmanaged.guidItem); return new NotifyIconData(_windowHandle, managedId, _unmanaged.uFlags) { CallbackMessage = _unmanaged.uCallbackMessage, Icon = _iconHandle, State = _unmanaged.dwState, StateMask = _unmanaged.dwStateMask, TimeoutOrVersion = _unmanaged.uTimeoutOrVersion, InfoFlags = _unmanaged.dwInfoFlags, BalloonIcon = _balloonIconHandle, Tip = new string(_unmanaged.Tip), Info = new string(_unmanaged.Info), InfoTitle = new string(_unmanaged.InfoTitle) }; } /// <summary> /// Releases all resources in use by the marshaller. /// </summary> public void Free() { if (_windowHandleAddRefd) _windowHandle.DangerousRelease(); if (_iconHandle != null && _iconHandleAddRefd) _iconHandle.DangerousRelease(); if (_balloonIconHandle != null && _balloonIconHandleAddRefd) _balloonIconHandle.DangerousRelease(); } } }
During managed-to-unmanaged conversion in the FromManaged
method, we call DangerousAddRef
on all the provided SafeHandle
instances and store the bool
value indicating whether or not the reference counter was successfully incremented.
After that, we call DangerousGetHandle
for each SafeHandle
present and store the resulting system handles both in our marshalling struct as well as the NOTIFYICONDATAW
struct which we’re populating.
We keep a backup of the original handles in order to see if their values have changed at all when the ToManaged
method executes; something which we cannot support given that they originated from SafeHandle
types.
Finally, in the Free
method, we make good on our attempt to keep our SafeHandle
instances “safe” and call the DangerousRelease
method for each handle whose reference counter was previously incremented.
SafeHandles and Structs Living in Harmony
We’re done! Our P/Invoke signature for Shell_NotifyIcon
should now be error-free.
Shell_NotifyIcon P/Invoke Signature
/// <summary> /// Sends a message to the taskbar's status area. /// </summary> /// <param name="dwMessage">A value that specifies the action to be taken by this function.</param> /// <param name="lpData"> /// A <see cref="NotifyIconData"/> instance containing notification area information. /// </param> /// <returns>True if successful; otherwise, false.</returns> [LibraryImport(LIBRARY_NAME, EntryPoint = "Shell_NotifyIconW")] [return: MarshalAs(UnmanagedType.Bool)] public static partial bool Shell_NotifyIcon(NotifyIconMessage dwMessage, ref NotifyIconData lpData);
Let’s try it out. The following code was tested and executed successfully.
Shell_NotifyIcon Usage with SafeHandles
var iconData = new NotifyIconData(windowHandle, _id, NotifyIconFlags.Message | NotifyIconFlags.Icon | NotifyIconFlags.Tip) { CallbackMessage = (uint) _TrayEvent, Icon = iconHandle, Tip = _tip }; Shell32.Shell_NotifyIcon(NotifyIconMessage.Add, ref iconData);
Mission accomplished! We can all rest easier at night with our SafeHandle
instances now being able to be passed into those pesky P/Invoke methods requiring C-style structs.