Properly declare SP_DEVICE_INTERFACE_DETAIL_DATA for PInvoke

柔情痞子 提交于 2019-12-23 07:29:09

问题


The SP_DEVICE_INTERFACE_DETAIL_DATA structure:

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

How do I declare it in C# to get Marshal.SizeOf work properly?

I don't have a problem with allocating dynamic buffer. I only want to calculate cbSize in a proper, non-hardcoded manner.

The definition at PInvoke.net is wrong.
The explanation at PInvoke.net is also wrong:

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

Don't trust him.
4 + Marshal.SystemDefaultCharSize is only valid on x86. Same for sizeof(int) + Marshal.SystemDefaultCharSize. On x64 it fails miserably.

This is what unmanaged C++ gives:

x86
Struct size A: 5
Offset of device path A: 4
Struct size W: 6
Offset of device path W: 4

x64
Struct size A: 8
Offset of device path A: 4
Struct size W: 8
Offset of device path W: 4

I tried every possible combination of StructLayout and MarshalAs parameters, but I couldn't get it to return the above values.

What is the correct declaration?


回答1:


The key point of the structure is that you don't know how large it should be. You have to call SetupDiGetDeviceInterfaceDetail() twice, on the first call you intentionally pass 0 for the DeviceInterfaceDetailSize argument. That will of course fail, but the RequiredSize argument will tell you how big the structure needs to be. Then you allocate a structure of the right size and call it again.

Dynamically sizing a structure is not directly supported by the pinvoke marshaller or the C# language. So declaring the structure isn't going to help at all, don't try. You should use Marshal.AllocHGlobal(). That gets you a pointer that you can pass as the DeviceInterfaceDetailData argument. Set cbSize with Marshal.WriteInt32. Now make the call. And retrieve the returned string with Marshal.PtrToStringUni(). Marshal.FreeHGlobal to clean up. You shouldn't have any trouble googling code that does this from the method names.


The cbSize member is a problem, the SetupApi.h SDK header file contains this:

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

That's fugly, a C compiler will think there is 2 bytes of padding after the array, even though there is not. In C# code the StructLayoutAttribute.Pack value needs to be different for 32-bit code vs 64-bit code. There is no way to cleanly do this without declaring two structures. And choose between them based on the value of IntPtr.Size. Or just hard-code it since the structure declaration isn't useful anyway, it is 6 in 32-bit mode and 8 in 64-bit mode. The string starts at offset 4 in both cases. Assuming Unicode strings of course, no point in using ansi strings.




回答2:


It's been quite a while, but this is the code I'm using (after reading through all these answers, and others online), which seems to work well on x86 and x64, as of the current version of Windows 10:

    [DllImport(@"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern Boolean SetupDiGetDeviceInterfaceDetail(
       IntPtr hDevInfo,
       ref SP_DEVINFO_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 does have the marshalling correct at the link JamieSee gave: http://social.msdn.microsoft.com/Forums/en/clr/thread/1b7be634-2c8f-4fc6-892e-ece97bcf3f0e

However, he did do the pointer arithmetic incorrectly:

Correct

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

Incorrect (can fail to produce correct value on x64)

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

The reason you're seeing the size values you got is because of padding. The padding is not relevant to the function being called. All that matters is that cbSize >= 4 on the first call (to get the actual size needed).




回答4:


you must do something at runtime.

code:

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



回答5:


I've only checked this on 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);
}

I looked at the code inside SetupApi.dll and the first thing the ASCII version does is check for a NULL on detail and then for the right cbSize against a hardcoded value. This is because the ASCII version feeds into the Widechar version.

You can't do this using the Widechar API with the first two parameters invalid. If you are using the Widechar API just WORD align the size from this function.

I'll be grateful if somebody could check this on other systems.



来源:https://stackoverflow.com/questions/10728644/properly-declare-sp-device-interface-detail-data-for-pinvoke

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!