24 December 2008

Structures that vary in size on x86/x64 - what to do?

Some structures can be difficult to marshal, as their size varies on x86 compared to x64:

typedef struct _TBBUTTON {
   int         iBitmap;
   int         idCommand;
   BYTE     fsState;
   BYTE     fsStyle;
#ifdef _WIN64
   BYTE     bReserved[6]     // padding for alignment
#elif defined(_WIN32)
   BYTE     bReserved[2]     // padding for alignment
#endif
   DWORD_PTR   dwData;
   INT_PTR          iString;
} TBBUTTON, NEAR *PTBBUTTON *LPTBBUTTON;
 
The layouts in x86 and x64 would look like this:
 
image 
 
In C the standard is for, say, an int to stay within a 4 byte boundary of the start of the structure. A short would
not cross a 2 byte boundary. Bytes can go where they like (well, they shouldn't cross the byte boundary, which I
guess they could by having bits spread over 2 bytes!) 
A pointer on x86 is a 4 byte value, and shouldn't cross a 4 byte boundary. A pointer on x64 is an 8 byte value, and
shouldn't cross an 8 byte boundary. Phew. Enough about byte boundaries...
TBBUTTON causes problems as it has the two BYTE fields in the middle. We need to pad, to get the dwData field
to start at a suitable location. Once it is in the correct place, iString will be fine.
With .Net we have a few ways to arrange structures. We can use StructLayout(LayoutKind.Sequential, Pack:=x).
Sequential means that .Net is not allowed to move the fields around at runtime, we want them to be arranged
in memory in the order that they appear in our declaration, this will match the C behaviour. Pack allows you to
specify the boundary to align the fields against. Often you would use sequential and pack = 1, then add fields of
padding to get it aligned.
Alternatively, you can use StructLayout(LayoutKind.Explicit) and hard-code where the fields should go. But,
this will only work if the structures have the same layout in x86 and x64. (Unfortunately you must use a
constant for the field offset, so you can't calculate it at runtime).
For TBBUTTON we are stuffed. There is no arrangement of pack and padding so that "one structure fits all", we will
need 2 declarations. And then we'll choose between them at runtime. (I think it might also be possible to just
compile it for x86 and run it under WOW64 on x64...)
<StructLayout(LayoutKind.Sequential, Pack:=1)> _
Public Structure TBBUTTON32
   Public bitmapIndex As Integer
   Public command As Integer
   Public state As TBStates
   Public style As TBStyles
   Public padding As UShort
   Public data As IntPtr
   Public iString As IntPtr
   Public Function Size() As Integer
       Return Marshal.SizeOf(Me)
   End Function
End Structure

<StructLayout(LayoutKind.Sequential, Pack:=1)> _
Public Structure TBBUTTON64
   Public bitmapIndex As Integer
   Public command As Integer
   Public state As TBStates
   Public style As TBStyles
   Public padding1 As Integer
   Public padding2 As UShort
   Public data As IntPtr
   Public iString As IntPtr
   Public Function Size() As Integer
       Return Marshal.SizeOf(Me)
   End Function
End Structure
The function is there for convenience. The 6 bytes of padding in the 64 bit version are spread across two variables.
We could equally use field offsets, but it takes a bit longer as you have to calculate them all. Unless you brain finds
that easy. My brain prefers arranging them in blocks and having padding bytes, and doesn't consider the numbers.

One thing that you think might work is the conditional compile thingy in VB.Net:
#if Platform="x86"
but this depends on the build target, it's not determined at runtime. So again, you would end up with two builds.

1 comment:

  1. Actually, you could be sneaky and use an IntPtr for all three fields - state, style and padding. Marshalled as SysUint. Then convert it later.

    ReplyDelete