aybe
aybe

Reputation: 16662

What are some good strategies for working with DeviceIoControl?

I am looking for some guidance when it comes to call DeviceIoControl from C#, knowing that its generic aspect of accepting pointer parameters isn't always easy to express in C#.

Here are two examples and explanations laid out below.

Example 1:

This works but is cumbersome, you have a disposable scope but you have to pass the parameters to the function and at the end assign the output buffer value back to the variable.

var toc = new CDROM_TOC(); // non blittable

var code = NativeConstants.IOCTL_CDROM_READ_TOC;

using (var scope = new UnmanagedMemoryScope<CDROM_TOC>(toc))
{
    if (!UnsafeNativeMethods.DeviceIoControl(Handle, code, IntPtr.Zero, 0, scope.Memory, scope.Size, out _))
        return Array.Empty<ITrack>();

    toc = scope.Value; // this is weird
}

Example 1 helper:

internal struct UnmanagedMemoryScope<T> : IDisposable where T : struct
{
    private bool IsDisposed { get; set; }
    public uint Size { get; }
    public IntPtr Memory { get; }

    public T Value
    {
        get => Marshal.PtrToStructure<T>(Memory);
        set => Marshal.StructureToPtr(value, Memory, true);
    }

    public UnmanagedMemoryScope(T value)
    {
        var size = Marshal.SizeOf<T>();
        Memory = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(value, Memory, false);
        Size = (uint)size;
        IsDisposed = false;
    }

    public void Dispose()
    {
        if (IsDisposed)
            return;

        if (Memory != default)
            Marshal.FreeHGlobal(Memory);

        IsDisposed = true;
    }
}

Example 2:

This one is already much more friendly, wrappers do marshalling and the value passed is ref.

var toc = new CDROM_TOC(); // non blittable

var code = NativeConstants.IOCTL_CDROM_READ_TOC;

var ioctl = DeviceIoControl(Handle, code, ref toc);

// ...

Example 2 helper 1:

private static bool DeviceIoControl<TTarget>(
    SafeFileHandle handle, uint code, ref TTarget target)
    where TTarget : struct
{
    var sizeOf = Marshal.SizeOf<TTarget>();
    var intPtr = Marshal.AllocHGlobal(sizeOf);

    Marshal.StructureToPtr(target, intPtr, false);

    var ioctl = UnsafeNativeMethods.DeviceIoControl(
        handle,
        code,
        IntPtr.Zero,
        0u,
        intPtr,
        (uint)sizeOf,
        out var lpBytesReturned
    );

    target = Marshal.PtrToStructure<TTarget>(intPtr);

    Marshal.FreeHGlobal(intPtr);

    return ioctl;
}

Example 2 helper 2:

private static bool DeviceIoControl<TTarget, TSource>(
    SafeFileHandle handle, uint code, ref TTarget target, ref TSource source)
    where TSource : struct 
    where TTarget : struct
{
    var sizeOf1 = Marshal.SizeOf(source);
    var sizeOf2 = Marshal.SizeOf(target);
    var intPtr1 = Marshal.AllocHGlobal(sizeOf1);
    var intPtr2 = Marshal.AllocHGlobal(sizeOf2);
    
    Marshal.StructureToPtr(source, intPtr1, false);
    Marshal.StructureToPtr(target, intPtr2, false);

    var ioctl = UnsafeNativeMethods.DeviceIoControl(
        handle,
        code,
        intPtr1,
        (uint)sizeOf1,
        intPtr2,
        (uint)sizeOf2,
        out var lpBytesReturned
    );

    Marshal.PtrToStructure(intPtr1, source);
    Marshal.PtrToStructure(intPtr2, target);
    
    Marshal.FreeHGlobal(intPtr1);
    Marshal.FreeHGlobal(intPtr2);

    return ioctl;
}

But I feel that I might be missing something and maybe there's a better approach...

Question:

What are some good tricks when it comes to call DeviceIoControl from C#?

Knowing that,

Of course there's the C++/CLI route but well, it's not C# anymore...

Hope that makes sense to you, else let me know.

Upvotes: -1

Views: 330

Answers (1)

Soonts
Soonts

Reputation: 21936

I usually do it like this.

Parameters structure:

ref struct CDROM_TOC
{
    const int MAXIMUM_NUMBER_TRACKS = 100;
    public const int sizeInBytes = 4 + MAXIMUM_NUMBER_TRACKS * 8;

    readonly Span<byte> buffer;

    public CDROM_TOC( Span<byte> buffer )
    {
        if( buffer.Length != sizeInBytes )
            throw new ArgumentException();
        this.buffer = buffer;
    }

    /// <summary>Fixed header of the structure</summary>
    public struct Header
    {
        public ushort length;
        public byte firstTrack, lastTrack;
    }

    /// <summary>Fixed header</summary>
    public ref Header header =>
        ref MemoryMarshal.Cast<byte, Header>( buffer.Slice( 0, 4 ) )[ 0 ];

    public struct TRACK_DATA
    {
        byte reserved;
        public byte controlAndAdr;
        public byte trackNumber;
        byte reserved2;
        public uint address;
    }

    /// <summary>Tracks collection</summary>
    public Span<TRACK_DATA> tracks =>
        MemoryMarshal.Cast<byte, TRACK_DATA>( buffer.Slice( 4 ) );

    // Make this structure compatible with fixed() statement
    public ref byte GetPinnableReference() => ref buffer[ 0 ];
}

Usage example:

CDROM_TOC toc = new CDROM_TOC( stackalloc byte[ CDROM_TOC.sizeInBytes ] );
unsafe
{
    fixed( byte* buffer = toc )
    {
        // Here you have unmanaged pointer for that C interop.
    }
}
// If you want to return the tracks, need to copy to managed heap:
var header = toc.header;
return toc.tracks
    .Slice( header.firstTrack, header.lastTrack - header.firstTrack + 1 )
    .ToArray();

Couple more notes.

The answer assumes you have a modern C#, i.e. .NET 5 or newer, or any version of .NET Core.

The example does use unsafe, but only on the lowest level. If you absolutely don’t want that, use GCHandle instead. With GCHandleType.Pinned, it’s an equivalent to unsafe keyword, only slower.

Unlike your code, this method does not use any heap memory for the interop, neither managed nor native.

The instance of the structure is stack allocated, and it exposes higher-level API to access the fields of that structure. The complete stack is already fixed in memory, the fixed keyword going to nothing for that code, just return the address. Doing nothing is free performance-wise.

Upvotes: 1

Related Questions