【问题标题】:Properly declare SP_DEVICE_INTERFACE_DETAIL_DATA for PInvoke为 PInvoke 正确声明 SP_DEVICE_INTERFACE_DETAIL_DATA
【发布时间】:2012-05-30 12:21:32
【问题描述】:

SP_DEVICE_INTERFACE_DETAIL_DATA 结构:

typedef struct _SP_DEVICE_INTERFACE_DETAIL_DATA {
  DWORD cbSize;
  TCHAR DevicePath[ANYSIZE_ARRAY];
} SP_DEVICE_INTERFACE_DETAIL_DATA, *PSP_DEVICE_INTERFACE_DETAIL_DATA;

如何在 C# 中声明它以使 Marshal.SizeOf 正常工作?

分配动态缓冲区没有问题。 我只想以适当的、非硬编码的方式计算cbSize

definition at PInvoke.net 是错误的。
PInvoke.net 上的解释也是错误的:

SP_DEVICE_INTERFACE_DETAIL_DATA didd = new SP_DEVICE_INTERFACE_DETAIL_DATA();
didd.cbSize = 4 + Marshal.SystemDefaultCharSize; // trust me :)

不要相信他。
4 + Marshal.SystemDefaultCharSize 仅在 x86 上有效。 sizeof(int) + Marshal.SystemDefaultCharSize 也一样。在 x64 上它失败得很惨。

这是非托管 C++ 所提供的:

x86
结构尺寸A:5
设备路径 A 的偏移量:4
结构尺寸W:6
设备路径W偏移量:4

x64
结构尺寸A:8
设备路径 A 的偏移量:4
结构尺寸W:8
设备路径W偏移量:4

我尝试了StructLayoutMarshalAs 参数的所有可能组合,但我无法让它返回上述值。

什么是正确的声明?

【问题讨论】:

标签: c# winapi pinvoke memory-alignment


【解决方案1】:

结构的关键在于你不知道它应该有多大。您必须调用 SetupDiGetDeviceInterfaceDetail() 两次,在第一次调用时您故意为 DeviceInterfaceDetailSize 参数传递 0。这当然会失败,但RequiredSize 参数会告诉您结构需要多大。然后你分配一个合适大小的结构并再次调用它。

pinvoke marshaller 或 C# 语言不直接支持动态调整结构大小。所以声明结构根本没有帮助,不要尝试。你应该使用 Marshal.AllocHGlobal()。这会为您提供一个指针,您可以将其作为 DeviceInterfaceDetailData 参数传递。使用 Marshal.WriteInt32 设置 cbSize。现在打电话。并使用 Marshal.PtrToStringUni() 检索返回的字符串。 Marshal.FreeHGlobal 进行清理。从方法名称中搜索执行此操作的代码应该没有任何问题。


cbSize 成员有问题,SetupApi.h SDK 头文件包含这个:

#ifdef _WIN64
#include <pshpack8.h>   // Assume 8-byte (64-bit) packing throughout
#else
#include <pshpack1.h>   // Assume byte packing throughout (32-bit processor)
#endif

这很糟糕,C 编译器会认为数组后面有 2 个字节的填充,即使没有。在 C# 代码中,32 位代码与 64 位代码的 StructLayoutAttribute.Pack 值需要不同。如果不声明 两个 结构,就无法干净地做到这一点。并根据 IntPtr.Size 的值在它们之间进行选择。或者只是硬编码它,因为结构声明无论如何都没有用,它在 32 位模式下是 6,在 64 位模式下是 8。在这两种情况下,字符串都从偏移量 4 开始。当然,假设 Unicode 字符串,使用 ansi 字符串没有意义。

【讨论】:

  • 如文档所述,cbSize 不包括可变部分。它的计算公式为sizeof(DWORD) + padding + sizeof(TCHAR) + padding。这里的困难部分是填充。在 x64 上,在 TCHAR 之后 有额外的填充;在 x86 上,没有。想象一下,您正在声明一个非可变长度结构,其中第二个成员是[MarshalAs(ByValArray, SizeConst=1)] public char[] foo。你是如何让它发挥作用的?
  • 当我打开你的答案时,我正盯着 SetupApi.h 中的这篇文章。似乎我将不得不做类似this的事情。
  • 不,不要那样做。当托管代码针对 AnyCPU 构建时,它可以在 32 位或 64 位模式下运行。 #ifdef 无法解决该问题,您必须在运行时进行。
【解决方案2】:

已经有一段时间了,但这是我正在使用的代码(在阅读了所有这些答案和其他在线答案之后),从当前版本的 Windows 10 开始,它似乎在 x86 和 x64 上运行良好:

    [DllImport(@"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern Boolean SetupDiGetDeviceInterfaceDetail(
       IntPtr hDevInfo,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData,
       IntPtr deviceInterfaceDetailData,
       int deviceInterfaceDetailDataSize,
       ref UInt32 requiredSize,
       ref SP_DEVINFO_DATA deviceInfoData
    );

    public static String GetDeviceInterfacePath(IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA devInfo, ref SP_DEVINFO_DATA deviceInterfaceData)
    {
        String devicePath = null;
        IntPtr detailData = IntPtr.Zero;
        UInt32 detailSize = 0;

        SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, ref deviceInterfaceData, detailData, 0, ref detailSize, ref devInfo);
        if (detailSize > 0)
        {
            int structSize = Marshal.SystemDefaultCharSize;
            if (IntPtr.Size == 8)
                structSize += 6;  // 64-bit systems, with 8-byte packing
            else
                structSize += 4; // 32-bit systems, with byte packing

            detailData = Marshal.AllocHGlobal((int)detailSize + structSize);
            Marshal.WriteInt32(detailData, (int)structSize);
            Boolean Success = SetupDiGetDeviceInterfaceDetail(DeviceInfoSet, ref deviceInterfaceData, detailData, (int)detailSize, ref detailSize, ref devInfo);
            if (Success)
            {
                devicePath = Marshal.PtrToStringUni(new IntPtr(detailData.ToInt64() + 4));
            }
            Marshal.FreeHGlobal(detailData);
        }

        return devicePath;
    }

【讨论】:

    【解决方案3】:

    Mike Danes 在 JamieSee 提供的链接中确实有正确的编组:http://social.msdn.microsoft.com/Forums/en/clr/thread/1b7be634-2c8f-4fc6-892e-ece97bcf3f0e

    但是,他确实做错了指针运算:

    正确

    detail = (IntPtr)(detail.ToInt64() + 4); //skip the cbSize field
    

    不正确的(可能无法在 x64 上产生正确的值)

    detail = (IntPtr)(detail.ToInt32() + 4); //skip the cbSize field
    

    您看到大小值的原因是由于填充。填充与被调用的函数无关。重要的是第一次调用时 cbSize >= 4(以获得所需的实际大小)。

    【讨论】:

    • 正如我在问题中指出的,这与编组无关。目前一切正常,但我有一个硬编码部分,我在其中输入了cbSize 的正确值(5、6 或 8,取决于当前的位数和字符宽度)。 cbSize 不依赖于缓冲区的实际长度。 4不是正确的cbSize。我想要做的是删除硬编码部分并让编组器正确计算大小。
    • 进一步澄清:cbSize 由接受它的系统函数严格检查。如果我在 x64 上将其设置为 6,则会失败。如果我在 x86 上将其设置为 8,则会失败。如果我在非 UNICODE x86 上将其设置为 6,则会失败。
    【解决方案4】:

    你必须在运行时做一些事情。

    代码:

    didd.cbSize = Marshal.SizeOf(typeof(Native.SP_DEVICE_INTERFACE_DETAIL_DATA));
    if (IntPtr.Size == 4)
    {
        didd.cbSize = 4 + Marshal.SystemDefaultCharSize;
     }
    

    【讨论】:

      【解决方案5】:

      我只在 XP 上检查过这个:

      DWORD get_ascii_detail_size(void)
      {
         DWORD detail[2], n;
      
         for(n=5;n<=8;n+=3)
         {
            detail[0]=n;
            SetupDiGetDeviceInterfaceDetailA(NULL, NULL, detail, n, NULL, NULL);
            if (GetLastError()!=ERROR_INVALID_USER_BUFFER) return(n);
         }
         return(0);
      }
      

      我查看了 SetupApi.dll 中的代码,ASCII 版本所做的第一件事是检查细节是否为 NULL,然后根据硬编码值检查正确的 cbSize。 这是因为 ASCII 版本馈入 Widechar 版本。

      如果前两个参数无效,则无法使用 Widechar API 执行此操作。如果您使用的是 Widechar API,只需 WORD 对齐此函数的大小。

      如果有人可以在其他系统上进行检查,我将不胜感激。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-31
        • 1970-01-01
        • 1970-01-01
        • 2022-11-18
        • 2023-03-16
        • 2018-04-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多