Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Properly declare SP_DEVICE_INTERFACE_DETAIL_DATA for PInvoke

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?

like image 741
GSerg Avatar asked May 23 '12 22:05

GSerg


2 Answers

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.

like image 148
Hans Passant Avatar answered Oct 18 '22 14:10

Hans Passant


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_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;
    }
like image 1
Jeff Hay Avatar answered Oct 18 '22 15:10

Jeff Hay