1

I'm using the PInvoke stuff in order to make use of the SetupAPI functions from C++. I'm using this to get paths to USB devices conforming to the HID spec. I've got everything working but something I don't understand has me puzzled. Using this structure from the SetupAPI:

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

I don't get the same results as the example code I'm using. First off, I'm using an IntPtr and allocating memory using Marshal.AllocHGlobal() to pass this back and forth. I call SetupDiGetDeviceInterfaceDetail() twice, first to get the size of the buffer I need, and second to actually get the data I'm interested in. I'm looking to get the Path to this device, which is stored in this struct.

The code I'm going off of does this:

IntPtr pDevPath = new IntPtr(pDevInfoDetail.ToInt32() + 4);
string path = Marshal.PtrToStringAuto(pDevPath);

Which works just fine. I did that and the string I got was gibberish. I had to change it to

IntPtr pDevPath = new IntPtr(pDevInfoDetail.ToInt32() + 4);
string path = Marshal.PtrToStringAnsi(pDevPath);

to make it work. Why is this? Am I missing some setting for the project/solution that informs this beast how to treat strings and chars? So far, the MSDN article for PtrToStringAuto() doesn't tell me much about it. In fact, it looks like this method should have made the appropriate decision, called either the Unicode or Ansi version for my needs, and all would be well.

Please explain.

ChrisWue
  • 18,612
  • 4
  • 58
  • 83
Andrew Falanga
  • 2,274
  • 4
  • 26
  • 51
  • 1
    Why don't you use existing pinvoke? http://pinvoke.net/default.aspx/setupapi/SetupDiGetDeviceInterfaceDetail.html – adontz Jan 11 '12 at 00:18

1 Answers1

1

First of all, +10000 on using a real P/Invoke interop type and not marshalling data by hand. But since you asked, here's what's going on with your strings.

The runtime decides how to treat strings and chars on a per-case basis, based on the attributes you apply to your interop declaractions, the context in which you use interop, the methods you call, etc. Every type of P/Invoke declaration (extern method, delegate, or structure) allows you to specify the default character size for the scope of that definision. There are three options:

  • Use CharSet.Ansi, which converts the managed Unicode strings to 8-bit characters
  • Use CharSet.Unicode, which passes the string data as 16-bit characters
  • Use CharSet.Auto, which decides at runtime, based on the host OS, which one to use.

In general, I hate CharSet.Auto because it's mostly pointless. Since the Framework doesn't even support Windows 95, the only time "Auto" doesn't mean "Unicode" is when running on Windows 98. But there's a bigger problem here, which is that the runtime's decision on how to marshal strings happens at the "wrong time".

The unmanaged code you are calling made that decision at compile time, since the compiler had to decide if TCHAR meant char or wchar -- that decision is based on the presence of the _UNICODE preprocessor macro. That means that, for most libraries, it's going to always use one or the other, and there's no point in letting the CLR "pick one".

For Windows system components, things are a bit better, because the Unicode-aware builds actually include two versions of most system function. The Setup API, for example, has two methods: SetupDiGetDeviceInterfaceDetailA and SetupDiGetDeviceInterfaceDetailW. The *A version uses 8-bit "ANSI" strings and the *W version uses 16-bit wide "Unicode" strings. It similarly has ANSI and Wide version of any structure which has a string.

This is the kind of situation where CharSet.Auto shines, assuming you use it properly. When you apply a DllImport to a function, you can specify the character set. If you specify Ansi for the character set, if the runtime doesn't find an exact match to your function name, it appends the A and tries again. (Oddly, if you specify Unicode, it will call the *W function first, and only try an exact match if that fails.)

Here's the catch: if you don't specify a character set on your DllImport, the default is CharSet.Ansi. This means you are going to get the ANSI version of the function, unless you specifically override the charset. That's most likely what is happening here: you are calling the ANSI version of SetupDiGetDeviceInterfaceDetail by default, and thus getting an ANSI string back, but PtrToStringAuto wants to use Unicode because you're probably running at least Windows XP.

The BEST option, assuming we can ignore Windows 98, would be to specify CharSet.Unicode all over the place, since SetupAPI supports it, but at the very least, you need to specify the same CharSet value everywhere.

Michael Edenfield
  • 28,070
  • 4
  • 86
  • 117
  • Thank you Michael for your detailed explanation. This is exactly what I was looking for. You're right on the money too. I looked at the PInvoke signature of this Setup API function in the example code I have and it is decorated with CharSet = CharSet.Auto. I'll be adding that to mine because this code should not be run on anything less than XP for Windows versions. – Andrew Falanga Jan 11 '12 at 19:21