Stop using IntPtr for dealing with system handles

 
 
  • Gérald Barré

When using system handles such as file handles, process handles, or any other handles provided by the kernel, you should take care to release them correctly when you don't need them anymore. The native APIs often provides a method to get a handle and a method to release it, plus sometimes a few methods to work with the resource. For instance, you can get a file handle using the method CreateFile, release it using CloseHandle, and for instance, write to the file using WriteFile. If you don't want to leak the resource, you need to ensure you call the method CloseHandle as soon as you have finished using the file. You must call this method once and only once. What if there is an exception in the code before you could release the handle. Maybe the exception is not directly from your code, but for instance, someone could abort the thread you are running on, or you could run out of memory.

SafeHandle was introduced in .NET but it is still not commonly used (except in the .NET Framework of course). SafeHandle provides many advantages:

  • The finalizer logic is already implemented, preventing you from doing mistakes while implementing them. Also, they inherit from CriticalFinalizerObject making them more reliable than other objects (could be freed even when ThreadAbortException or OutOfMemoryException are raised).
  • The objects that own a SafeHandle do not need to have a finalizer, so they are easier to write.
  • Objects with finalizer have special treatments in the Garbage Collector. You should keep them as small as possible to avoid promoting a large object graph due to finalization. SafeHandles are minimal wrappers around unmanaged resources, so it reduce the issue.
  • SafeHandles are well-integrated with P/Invoke. You can use SafeHandle-derived class in the definition of the method instead of IntPtr, so they are strongly-typed.
  • SafeHandles are well-integrated with the Garbage Collector. You don't need to use HandleRef and GC.KeepAlive explicitly when calling a native method, the CLR will do what is needed to avoid finalizing the SafeHandle while calling a native method.
  • SafeHandles prevent handle-recycling vulnerability: Lifetime, GC.KeepAlive, handle recycling

SafeHandle is an abstract class, so you need to inherit from it. When you inherit from SafeHandle, you must override the following members: IsInvalid and ReleaseHandle. You should also provide a default constructor that calls the base constructor with a value that represents an invalid handle value, and a boolean value indicating whether the native handle is owned by the SafeHandle and consequently should be freed when that SafeHandle has been disposed. Note that the ReleaseHandle method is guaranteed to be called only once and only if the handle is valid as defined by the IsInvalid property. You can also inherit from SafeHandleZeroOrMinusOneIsInvalid which handles the invalid handle.

C#
// inherits from SafeHandleZeroOrMinusOneIsInvalid, so IsInvalid is already implemented.
internal sealed class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    // A default constructor is required for P/Invoke to instantiate the class
    public MySafeHandle()
        : base(ownsHandle: true)
    {
    }

    protected override bool ReleaseHandle()
    {
        return NativeMethods.CloseHandle(handle);
    }
}

internal static class NativeMethods
{
    // Returns the SafeHandle instead of IntPtr
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    internal extern static MySafeHandle CreateFile(String fileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, IntPtr securityAttrs_MustBeZero, System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr TemplateFile_MustBeZero);

    // Take a SafeHandle in parameter instead of IntPtr
    [DllImport("kernel32", SetLastError = true)]
    internal extern static int ReadFile(MySafeHandle handle, byte[] bytes, int numBytesToRead, out int numBytesRead, IntPtr overlapped_MustBeZero);

    [DllImport("kernel32", SetLastError = true)]
    internal extern static bool CloseHandle(IntPtr handle);
}

As all the hard work is done by the SafeHandle, your code is much simpler! Here's an example of usage of MySafeHandle:

C#
public sealed class MyFileWrapper : IDisposable
{
    private readonly MySafeHandle _handle;

    public MyFileWrapper(string fullPath)
    {
        _handle = NativeMethods.CreateFile(fullPath, ...);
    }

    // - There is no need to implement a finalizer, MySafeHandle already has one
    // - You do not need to protect against multiple disposing, MySafeHandle already does
    public void Dispose()
    {
        _handle.Dispose();
    }
}

#Guidelines

When working with unmanaged resources, you should consider:

  1. Using an existing SafeHandle if possible
  2. If not possible, subclass SafeHandle to create one that meets your needs. This class should not do anything more than managing unmanaged resources. It should be sealed.
  3. If that's not possible, create your class which implements IDisposable and a finalizer
    1. The class should be sealed
    2. If sealing the class is not possible, add a method protected void Dispose(bool disposing), so subclasses can implements the dispose pattern correctly.

#Resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub