正确声明PInvoke中的SP_DEVICE_INTERFACE_DETAIL_DATA

9

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

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参数组合,但无法得到上述值。
请问正确的声明是什么?

请查看http://social.msdn.microsoft.com/Forums/en/clr/thread/1b7be634-2c8f-4fc6-892e-ece97bcf3f0e。 - JamieSee
@JamieSee 我试过了,也是错误的。4不是一个有效的尺寸。 - GSerg
5个回答

9
这种结构的关键在于,你不知道它应该有多大。你需要调用两次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编译器将认为数组后有两个字节的填充,但实际上并没有。在C#代码中,StructLayoutAttribute.Pack值需要针对32位和64位代码进行不同设置。没有干净利落的方法可以处理这种情况,除非声明两个结构体,并根据IntPtr.Size的值选择其中一个。或者只是硬编码,因为结构声明无用,32位模式下为6,64位模式下为8。字符串在两种情况下都从偏移量4开始。当然,假定使用Unicode字符串,没有使用ansi字符串的必要。

根据文档记录,cbSize 不包括变量部分。它的计算方式为 sizeof(DWORD) + 填充 + sizeof(TCHAR) + 填充。这里的难点在于填充。在 x64 上,在 TCHAR 之后有额外的填充;而在 x86 上,则没有。想象一下你正在声明一个非可变长度结构,其中第二个成员是 [MarshalAs(ByValArray, SizeConst=1)] public char[] foo。你该如何让它工作? - GSerg
当我打开你的回答时,我正在盯着SetupApi.h中的这个部分。看起来我要做类似于这样的事情:这里 - GSerg
4
不,不要这样做。当托管代码被构建为目标平台为 AnyCPU 时,它可以在 32 位或 64 位模式下运行。 #ifdef 无法解决这个问题,必须在运行时进行处理。 - Hans Passant

1

我已经有一段时间没有更新了,但这是我正在使用的代码(在阅读所有这些答案和其他在线资源后),它似乎在当前版本的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;
    }

0

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

代码:

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

0

我只在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版本的第一件事就是检查detail是否为NULL,然后检查cbSize是否与硬编码值匹配。这是因为ASCII版本会传递到Widechar版本。

如果您正在使用Widechar API,并且前两个参数无效,则无法执行此操作。从此函数中对大小进行WORD对齐即可。

如果有人能在其他系统上检查这个问题,我将不胜感激。


0

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(以获取实际所需大小)。

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

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