使用 DeviceIoControl 有哪些好的策略?

What are some good strategies for working with DeviceIoControl?

我正在寻找有关从 C# 调用 DeviceIoControl 的一些指导,我知道它的 generic 接受指针参数的方面并不总是容易表达在 C# 中。

下面是两个示例和解释。

示例 1:

这可行但很麻烦,你有一个一次性范围但你必须将参数传递给函数并在最后将输出缓冲区值分配回变量。

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
}

示例 1 助手:

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

示例 2:

这个已经友好多了,wrappers 进行编组,传递的值是 ref

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

var code = NativeConstants.IOCTL_CDROM_READ_TOC;

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

// ...

示例 2 助手 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;
}

示例 2 助手 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;
}

但我觉得我可能遗漏了一些东西,也许有更好的方法...

问题:

从 C# 调用 DeviceIoControl 时有哪些好的技巧?

知道了,

当然有 C++/CLI 路线,但好吧,它不再是 C#...

希望这对你有意义,否则请告诉我。

我一般都是这样

参数结构:

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

用法示例:

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

更多笔记。

答案假定您拥有现代 C#,即 .NET 5 或更新版本,或任何版本的 .NET Core。

该示例确实使用了 unsafe,但仅限于最低级别。如果您绝对不想那样,请改用 GCHandle。使用GCHandleType.Pinned,相当于unsafe关键字,只是速度较慢。

与您的代码不同,此方法不为互操作使用任何堆内存,既不托管也不本机。

结构的实例是堆栈分配的,它公开更高级别 API 以访问该结构的字段。完整的堆栈已经固定在内存中,fixed 关键字对该代码无效,只是 return 地址。无所事事在性能方面是免费的。