Managed interop for native overlapped

Previously, I shared a basic sample for overlapped I/O with modern C++. I say “basic” but truth be told, there are multiple subtle aspects that one must be keenly aware of to avoid memory leaks or corruption. Thankfully, most of the time we don’t need to worry about such things in the managed .NET world; we get the benefits of true async stream I/O for (nearly) free.

But what if we want to worry about this? That is to say, what if we have the need (I probably wouldn’t say “desire”) to call a native API which allows for overlapped I/O but has no built-in managed interface? One such example I am aware of is AttachVirtualDisk. Well, strap in and get acquainted with Overlapped.Pack, ThreadPool.BindHandle, and IOCompletionCallback.

But what luck! Junfeng Zhang has done most of the hard work for us and come up with a relatively complete sample in his “ThreadPool.BindHandle” blog post. Rather than regurgitate that here (the use case he demonstrates is run-of-the-mill file handles), I will show a sample using the VHD API I mentioned above since there are some key differences. In addition, since we are in the .NET 4.0+ world, I will show a simple way to implement the Task-based async pattern for the same.

Here is the basic interface we’ll be implementing:

public sealed class VirtualDisk : IDisposable
{
    private readonly SafeFileHandle handle;

    private VirtualDisk(SafeFileHandle handle)
    {
        this.handle = handle;
    }

    public static Task<VirtualDisk> OpenAsync(string path)
    {
        throw new NotImplementedException();
    }

    public Task AttachAsync()
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        using (this.handle)
        {
        }

        GC.SuppressFinalize(this);
    }

    private static class NativeMethods
    {
    }
}

Note that the constructor is private. Oddly enough, there is no non-blocking API to open a virtual disk. We’ll be forced to offload the OpenVirtualDisk call to the thread pool which means we can’t use the real constructor for this work. Since the constructor uses a raw-ish handle, we’ll hide it and use a named constructor to make the friendly interface we want. The empty NativeMethods class will eventually hold all the interop definitions, of which there will be quite a few.

Let’s start with the Open implementation:

public static async Task<VirtualDisk> OpenAsync(string path)
{
    await Task.Yield();
    NativeMethods.VIRTUAL_STORAGE_TYPE storageType = new NativeMethods.VIRTUAL_STORAGE_TYPE
    {
        DeviceId = NativeMethods.VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
        VendorId = NativeMethods.VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT
    };
    NativeMethods.OPEN_VIRTUAL_DISK_PARAMETERS parameters = new NativeMethods.OPEN_VIRTUAL_DISK_PARAMETERS
    {
        Version = NativeMethods.OPEN_VIRTUAL_DISK_VERSION.OPEN_VIRTUAL_DISK_VERSION_1
    };
    SafeFileHandle handle;
    int error = NativeMethods.OpenVirtualDisk(
        ref storageType,
        path,
        NativeMethods.VIRTUAL_DISK_ACCESS_MASK.VIRTUAL_DISK_ACCESS_ALL,
        NativeMethods.OPEN_VIRTUAL_DISK_FLAG.OPEN_VIRTUAL_DISK_FLAG_NONE,
        ref parameters,
        out handle);
    if (error != 0)
    {
        throw new Win32Exception(error);
    }

    return new VirtualDisk(handle);
}

…and the native interop definitions to support it:

public const uint VIRTUAL_STORAGE_TYPE_DEVICE_VHD = 2;

public static readonly Guid VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT = new Guid("EC984AEC-A0F9-47e9-901F-71415A66345B");

[StructLayout(LayoutKind.Sequential)]
public struct VIRTUAL_STORAGE_TYPE
{
    public uint DeviceId;
    public Guid VendorId;
}

[Flags]
public enum VIRTUAL_DISK_ACCESS_MASK
{
    VIRTUAL_DISK_ACCESS_NONE = 0x00000000,
    VIRTUAL_DISK_ACCESS_ATTACH_RO = 0x00010000,
    VIRTUAL_DISK_ACCESS_ATTACH_RW = 0x00020000,
    VIRTUAL_DISK_ACCESS_DETACH = 0x00040000,
    VIRTUAL_DISK_ACCESS_GET_INFO = 0x00080000,
    VIRTUAL_DISK_ACCESS_CREATE = 0x00100000,
    VIRTUAL_DISK_ACCESS_METAOPS = 0x00200000,
    VIRTUAL_DISK_ACCESS_READ = 0x000d0000,
    VIRTUAL_DISK_ACCESS_ALL = 0x003f0000,
    VIRTUAL_DISK_ACCESS_WRITABLE = 0x00320000
}

[Flags]
public enum OPEN_VIRTUAL_DISK_FLAG
{
    OPEN_VIRTUAL_DISK_FLAG_NONE = 0x00000000,
    OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS = 0x00000001,
    OPEN_VIRTUAL_DISK_FLAG_BLANK_FILE = 0x00000002,
    OPEN_VIRTUAL_DISK_FLAG_BOOT_DRIVE = 0x00000004,
    OPEN_VIRTUAL_DISK_FLAG_CACHED_IO = 0x00000008,
    OPEN_VIRTUAL_DISK_FLAG_CUSTOM_DIFF_CHAIN = 0x00000010
}

public enum OPEN_VIRTUAL_DISK_VERSION
{
    OPEN_VIRTUAL_DISK_VERSION_UNSPECIFIED = 0,
    OPEN_VIRTUAL_DISK_VERSION_1 = 1,
    OPEN_VIRTUAL_DISK_VERSION_2 = 2
}

[StructLayout(LayoutKind.Explicit)]
public struct OPEN_VIRTUAL_DISK_PARAMETERS
{
    [FieldOffset(0)]
    public OPEN_VIRTUAL_DISK_VERSION Version;

    [FieldOffset(4)]
    public uint RWDepth;

    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.Bool)]
    public bool GetInfoOnly;

    [FieldOffset(8)]
    [MarshalAs(UnmanagedType.Bool)]
    public bool ReadOnly;

    [FieldOffset(12)]
    public Guid ResiliencyGuid;
}

[DllImport("virtdisk.dll", ExactSpelling = true, CharSet = CharSet.Unicode, ThrowOnUnmappableChar = true)]
public static extern int OpenVirtualDisk(
    [In] ref VIRTUAL_STORAGE_TYPE VirtualStorageType,
    string Path,
    VIRTUAL_DISK_ACCESS_MASK VirtualDiskAccessMask,
    OPEN_VIRTUAL_DISK_FLAG Flags,
    [In] ref OPEN_VIRTUAL_DISK_PARAMETERS Parameters,
    out SafeFileHandle Handle);

This is all pretty standard stuff. Get a handle or else throw an exception on error. On success, we wrap the handle in VirtualDisk and allow the user to dispose of it at will.

Now for the more interesting Attach method:

// ...
private TaskCompletionSource<bool> attachOperation;
// ...

public Task AttachAsync()
{
    bool bound = ThreadPool.BindHandle(this.handle);
    if (!bound)
    {
        throw new InvalidOperationException("ThreadPool.BindHandle failed.");
    }

    this.attachOperation = new TaskCompletionSource<bool>();
    NativeMethods.ATTACH_VIRTUAL_DISK_PARAMETERS parameters = new NativeMethods.ATTACH_VIRTUAL_DISK_PARAMETERS
    {
        Version = NativeMethods.ATTACH_VIRTUAL_DISK_VERSION.ATTACH_VIRTUAL_DISK_VERSION_1
    };

    unsafe
    {
        Overlapped overlapped = new Overlapped();
        NativeOverlapped* nativeOverlapped = overlapped.Pack(this.OnAttachCompleted, null);
        try
        {
            int error = NativeMethods.AttachVirtualDisk(
                this.handle,
                IntPtr.Zero,
                NativeMethods.ATTACH_VIRTUAL_DISK_FLAG.ATTACH_VIRTUAL_DISK_FLAG_NONE,
                0,
                ref parameters,
                nativeOverlapped);
            if (error != 0 && error != NativeMethods.ERROR_IO_PENDING)
            {
                throw new Win32Exception(error);
            }
        }
        catch (Exception)
        {
            Overlapped.Free(nativeOverlapped);
            throw;
        }
    }

    return this.attachOperation.Task;
}

Note that I have made no attempt at thread safety here. (If you need that, you might consider using some sort of CompareExchange operation on the TaskCompletionSource object.) Anyway, the high points are the BindHandle call to set up I/O completion handling, the allocation of the native overlapped pointer (passing null to Pack since we don’t have a “buffer” as such), and the handling to ensure that on error exit we free it. If everything goes well, we return the tracking task and presume that the IOCompletionCallback will take over from here on. Here it is now:

private unsafe void OnAttachCompleted(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP)
{
    try
    {
        if (errorCode == 0)
        {
            this.attachOperation.SetResult(false);
        }
        else
        {
            this.attachOperation.SetException(new Win32Exception((int)errorCode));
        }
    }
    finally
    {
        Overlapped.Free(pOVERLAP);
    }
}

Again, we need to free that overlapped structure no matter what. Other than that, we’re just setting the task completion status based on what we were told by Windows. Finally, the remaining interop definitions:

public const int ERROR_IO_PENDING = 997;

[Flags]
public enum ATTACH_VIRTUAL_DISK_FLAG
{
    ATTACH_VIRTUAL_DISK_FLAG_NONE = 0x00000000,
    ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY = 0x00000001,
    ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER = 0x00000002,
    ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME = 0x00000004,
    ATTACH_VIRTUAL_DISK_FLAG_NO_LOCAL_HOST = 0x00000008
}

[StructLayout(LayoutKind.Explicit)]
public struct ATTACH_VIRTUAL_DISK_PARAMETERS
{
    [FieldOffset(0)]
    public ATTACH_VIRTUAL_DISK_VERSION Version;

    [FieldOffset(4)]
    public uint Reserved;
}

public enum ATTACH_VIRTUAL_DISK_VERSION
{
    ATTACH_VIRTUAL_DISK_VERSION_UNSPECIFIED = 0,
    ATTACH_VIRTUAL_DISK_VERSION_1 = 1
}

[DllImport("virtdisk.dll", ExactSpelling = true)]
public static extern unsafe int AttachVirtualDisk(
    SafeFileHandle VirtualDiskHandle,
    IntPtr SecurityDescriptor,
    ATTACH_VIRTUAL_DISK_FLAG Flags,
    uint ProviderSpecificFlags,
    ref ATTACH_VIRTUAL_DISK_PARAMETERS Parameters,
    [In] NativeOverlapped* Overlapped);

Whew! Lots of typing but it’s mostly just P/Invoke ceremony. All in all, it’s not terribly hard to use the native overlapped pattern from managed code. In fact, it even works in practice if this sample program and screen shot is proof enough:

// NOTE: This program must be run with elevated privileges to have permissions
//       to attach a VHD.
internal sealed class Program
{
    private static readonly Stopwatch MyStopwatch = Stopwatch.StartNew();

    private static void Main(string[] args)
    {
        MainAsync().Wait();
    }

    private static async Task MainAsync()
    {
        Log("Opening...");
        using (VirtualDisk disk = await VirtualDisk.OpenAsync(@"C:\Temp\my.vhd"))
        {
            Log("Attaching...");
            await disk.AttachAsync();

            Log("Attached!");

            Console.ReadLine();
        }
    }

    private static void Log(string message)
    {
        Console.WriteLine("[{0:000.000}/{1}] {2}", MyStopwatch.Elapsed.TotalSeconds, Thread.CurrentThread.ManagedThreadId, message);
    }
}
Sample VHD attached

Sample VHD attached

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.