Understanding SafeHandle pattern and usage

49 Views Asked by At

I have been reading some material online such as:

So the pattern to use in P/Invoke is such as:

// 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);
}

Well not quite... If I understand properly the correct pattern would be more like this instead:

internal static class NativeMethods
{
    [...] CreateFile / CloseHandle unchnaged [...]

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

    internal static int ReadFileSafe(MySafeHandle handle, byte[] bytes, int numBytesToRead, out int numBytesRead, IntPtr overlapped_MustBeZero) {
      if(handle.IsInvalid) throw new Exception("Invalid handle");
      return ReadFile(handle, bytes, numBytesToRead, numBytesRead, overlapped_MustBeZero);
    }

}

Did I understand the scope of SafeHandle properly ? In that case, is there any mechanism to avoid this boilerplate code ?

1

There are 1 best solutions below

0
malat On

So indeed SafeHandle simply expose the state of the internal handle. Therefore, one can keep API simple using:

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);
}

But implement the checking of state as:

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

    public MyFileWrapper(string fullPath)
    {
        _handle = NativeMethods.CreateFile(fullPath, ...);
        if(_handle.IsInvalid) throw new IOException("invalid handle");
    }

    // - 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();
    }
}

This is also documented in one of the example of the documentation online: