Pinvoke DeviceIoControl 参数

13
我正在进行一个使用 DeviceIoControl 的 C# 项目。我参考了相关的 Pinvoke.net页面 来获取我的签名:
[DllImport("Kernel32.dll", SetLastError = false, CharSet = CharSet.Auto)]
public static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    EIOControlCode IoControlCode,

    [MarshalAs(UnmanagedType.AsAny)]
    [In] object InBuffer,
    uint nInBufferSize,

    [MarshalAs(UnmanagedType.AsAny)]
    [Out] object OutBuffer,
    uint nOutBufferSize,

    out uint pBytesReturned,
    [In] IntPtr Overlapped
    );

我以前从未见过object[MarshalAs(UnmanagedType.AsAny)],但MSDN documentation听起来很有前途:

动态类型,在运行时确定对象的类型并将对象作为该类型进行编组。此成员仅对平台调用方法有效。

我的问题是:使用这个签名的“最佳”和/或“适当”方式是什么?

例如,IOCTL_STORAGE_QUERY_PROPERTY 需要 InBuffer 是一个 STORAGE_PROPERTY_QUERY 结构体。看起来我应该能够定义这个结构体,创建一个 new 实例,并将其传递给我的 Pinvoke 签名:
var query = new STORAGE_PROPERTY_QUERY { PropertyId = 0, QueryType = 0 };
DeviceIoControl(..., query, Marshal.SizeOf(query), ...);

然而,我刚想这样做就遇到了 System.ExecutionEngineException 错误,所以我改用了以下方法:

int cb = Marshal.SizeOf(typeof(...));
IntPtr query = Marshal.AllocHGlobal(cb);
...
Marshal.PtrToStructure(...);
Marshal.FreeHGlobal(query);

它至少在我调用时没有抛出任何异常。但这实在是太丑陋了,而且非常麻烦。难道 marshaller 不能像我希望的那样处理将数据复制到/从我的本地结构体中吗?
输出数据有时可能很棘手,因为它们不是固定大小的结构。我理解 marshaller 不可能自动处理这个问题,我可以接受在需要的地方进行 HGlobal 和复制操作。
另外:
一开始看起来这个问题很有帮助,但最终只是一个错误的常量。
我不反对使用 unsafe 构造。(fixed-size 结构成员需要这样做。)

我知道@HansPassant会知道这个答案 :-) - Jonathon Reinhart
是的,我也希望那样。 - David Heffernan
1个回答

24

DeviceIoControl很不友好。但是你可以让它变得不那么痛苦,你不必自己编写复杂的结构体转换代码。有两件事情可以利用:C#支持方法重载和PInvoke marshaller会相信你,即使你撒谎声明。这对于结构体来说非常完美,它们已经被封送为一块字节。这正是DeviceIoControl()所需要的。

因此,通用声明将如下所示:

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    int IoControlCode,
    byte[] InBuffer,
    int nInBufferSize,
    byte[] OutBuffer,
    int nOutBufferSize,
    out int pBytesReturned,
    IntPtr Overlapped
);

如果你对 IOCTL_STORAGE_QUERY_PROPERTY 感兴趣并希望其返回 STORAGE_DEVICE_DESCRIPTOR,那么你可以添加一个适用于此的重载。

[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    EIOControlCode IoControlCode,
    ref STORAGE_PROPERTY_QUERY InBuffer,
    int nInBufferSize,
    out STORAGE_DEVICE_DESCRIPTOR OutBuffer,
    int nOutBufferSize,
    out int pBytesReturned,
    IntPtr Overlapped
);

你可以这样调用它:

var query = new STORAGE_PROPERTY_QUERY { PropertyId = 0, QueryType = 0 };
var qsize = Marshal.SizeOf(query);
STORAGE_DEVICE_DESCRIPTOR result;
var rsize = Marshal.SizeOf(result);
int written;
bool ok = DeviceIoControl(handle, EIOControlCode.QueryProperty, 
             ref query, qsize, out result, rsize, out written, IntPtr.Zero);
if (!ok) throw new Win32Exception();
if (written != rsize) throw new InvalidOperationException("Bad structure declaration");

应该比你现有的看起来更美观和更易于诊断。未经测试,应该是接近的。


1
"As Any" 在 VB6 互操作中使用,我不知道它的确切语义。 "ref" 和 "out" 关键字确保生成指向结构体的指针。因此,您会得到一个 LPVOID。您可以使用类,但是然后放弃 ref/out 并明确写出 [Out]。如果您想调试它,那么您可以简单地创建一个 C DLL,其中包含具有完全相同签名的函数。 - Hans Passant
+1,编写重载函数(每个函数使用自己的struct类型作为参数)是使用DeviceIoControl的正确方法。 - ken2k
某些DeviceIoControl操作需要输入/输出缓冲区与扇区/页对齐,如果是这种情况,您需要使用VirtualAlloc手动分配内存。 - user1985513
1
不,他们没有这个要求。可能是驱动程序的问题,从来不是用户程序的问题。 - Hans Passant
1
没关系,这只是一个数字。在重载中使用枚举是明智的选择。 - Hans Passant
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接