Post

Tip: Interop Structures - To String or to Char(), that is the question.

The following is from Adam Nathan’s CLR FAQ discussing why you would use char arrays instead of strings when using Interop. If your using Interop, this is something very, very important. I’ve converted the code in the text to VB.NET ;-)


If you define a structure field as a string, it is marshaled as a pointer to an unmanaged string by default (LPSTR/LPWSTR). But with MarshalAsAttribute and UnmanagedType.ByValTStr, you can marshal a managed string field as an embedded string instead. This must be used with MarshalAsAttribute’s SizeConst named parameter to specify the size of the string. The CharSet marking in the structure’s StructLayoutAttribute determines whether the string is marshaled as ANSI or Unicode.

So it would seem natural to define DEVMODE’s string fields like this:

1
2
  <MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)>
  Public dmFormName As String

instead of this:

1
2
  <MarshalAs(UnmanagedType.ByValArray, SizeConst=32)>
  Public dmFormName As char()

The problem with ByValTStr is that the marshaler only copies SizeConst-1 characters, plus a trailing null character, into the unmanaged buffer. This almost certainly isn’t the behavior you’d want, but you can get away with it if you know that you’re never going to make use of all the characters. Or if the location of the field and the padding of the structure lets you get away with extending the size of the string by one character without affecting other fields, you could potentially set SizeConst to n+1 to work around this problem. But this is rarely the case. So to get the full length of the string, you often have to use ByValArray, as I did with my DEVMODE definition.

You can see exactly how structures get marshaled to unmanaged code by using the Visual Studio .NET memory window while debugging. To quickly show you the unmanaged memory layout corresponding to each definition of the dmFormName field above, I dusted off the MarshaledStructInspector class from Chapter 19 of .NET and COM: The Complete Interoperablity Guide. With the character array version, the following code:

1
2
3
4
5
6
7
8
  Dim d As DEVMODE = New DEVMODE()
  d.dmCollate = &H1234
  d.dmFormName = New char(){"a","b","c","d","e","f", _
    "g","h","i","j","k","l","m","n","o","p","q","r", _
    "s","t","u","v","w","x","y","z","A","B","C","D", _
    "E","F"}
  d.dmLogPixels = &H6789
  MarshaledStructInspector.DisplayStruct(d)

outputs:

  Total Bytes = 220
  ...
  34 12 61 00   4↕a.
  62 00 63 00   b.c.
  64 00 65 00   d.e.
  66 00 67 00   f.g.
  68 00 69 00   h.i.
  6A 00 6B 00   j.k.
  6C 00 6D 00   l.m.
  6E 00 6F 00   n.o.
  70 00 71 00   p.q.
  72 00 73 00   r.s.
  74 00 75 00   t.u.
  76 00 77 00   v.w.
  78 00 79 00   x.y.
  7A 00 41 00   z.A.
  42 00 43 00   B.C.
  44 00 45 00   D.E.
  46 00 89 67   F.?g
  ...

With the string version, the following code:

1
2
3
4
5
  Dim d as DEVMODE = New DEVMODE()
  d.dmCollate = &H1234
  d.dmFormName = "abcdefghijklmnopqrstuvwxyzABCDEF"
  d.dmLogPixels = &H6789
  MarshaledStructInspector.DisplayStruct(d)

outputs:

  Total Bytes = 220
  ...
  34 12 61 00   4↕a.
  62 00 63 00   b.c.
  64 00 65 00   d.e.
  66 00 67 00   f.g.
  68 00 69 00   h.i.
  6A 00 6B 00   j.k.
  6C 00 6D 00   l.m.
  6E 00 6F 00   n.o.
  70 00 71 00   p.q.
  72 00 73 00   r.s.
  74 00 75 00   t.u.
  76 00 77 00   v.w.
  78 00 79 00   x.y.
  7A 00 41 00   z.A.
  42 00 43 00   B.C.
  44 00 45 00   D.E.
  00 00 89 67   ..?g
  ...

Notice that the last character of the string is cut off.

As one final note, the Interop marshaler does not support StringBuilder fields. So unlike the case with parameters, StringBuilder can’t be used to represent unmanaged string fields.

This post is licensed under CC BY 4.0 by the author.