问题
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