Properly declare SP_DEVICE_INTERFACE_DETAIL_DATA for PInvoke
Asked Answered
O

5

9

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?

Osage answered 23/5, 2012 at 22:13 Comment(2)
Have a look at social.msdn.microsoft.com/Forums/en/clr/thread/…Chambermaid
@Chambermaid I have. It's wrong too. 4 is not a valid size.Osage
P
9

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.

Phonometer answered 24/5, 2012 at 0:2 Comment(3)
As documented, cbSize does not include the variable part. It is calculated as sizeof(DWORD) + padding + sizeof(TCHAR) + padding. The difficult part here is padding. On x64, there's additional padding after the TCHAR; on x86, there is not. Imagine you are declaring a non-variable-length structure with the second member being [MarshalAs(ByValArray, SizeConst=1)] public char[] foo. How do you make it work?Osage
I was staring at exactly this piece from SetupApi.h when I opened your answer. Seems like I'm going to have to do something like this.Osage
No, don't do that. Managed code can run in either 32-bit or 64-bit mode when it was built to target AnyCPU. An #ifdef cannot solve that problem, you have to do it at runtime.Phonometer
G
1

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;
    }
Gisele answered 8/6, 2016 at 21:57 Comment(0)
D
0

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).

Diao answered 23/5, 2012 at 23:21 Comment(2)
As I noted in the question, it's not about marshalling. Currently everything works, but I have a hardcoded part, where I put the correct values for cbSize (5, 6, or 8, depending on current bitness and char wideness). cbSize does not depend on the actual length of the buffer. 4 is not a correct cbSize. What I want to do is to remove the hardcoded part and make the marshaler calculate the size properly.Osage
To clarify even further: cbSize is strictly checked by the system functions that accept it. If I set it to 6 on x64, it fails. If I set it to 8 on x86, it fails. If I set it to 6 on non-UNICODE x86, it fails.Osage
A
0

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;
 }
Abirritant answered 10/4, 2013 at 7:26 Comment(0)
A
0

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.

Alfonse answered 20/11, 2013 at 20:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.