I have a C# service that collects data from another company that we do business with. They send the data in a binary format from one of their C++ applications. To read their data with .NET, I needed to marshal their data to a set of structs defined in C#. I created a structure that looked something like this.
[StructLayout(LayoutKind.Explicit, Size = 48)]
public struct SampleHeader
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
[FieldOffset(0)]
public byte[] RecordType;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(8)]
public uint Version;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
[FieldOffset(12)]
public byte[] SystemCode;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(20)]
public uint LocalID;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(24)]
public uint HostID;
}
The actual struct had more fields, but this is enough to show the the problem. During development and testing, the code worked fine. Until we tried it on a 64-bit edition of Windows Server 2003. That’s when it broke. As soon as I instantiated an instance of this struct, the service would throw an error. Something like this:
System.TypeLoadException:
Could not load type 'SampleNameSpace.SampleHeader'
from assembly ''Sample, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null'
because it contains an object field at offset 12 that is incorrectly aligned or overlapped by a non-object field.
To get it to fail, all I needed to do was to create a SampleHeader like this:
SampleHeader sh = new SampleHeader();
That didn’t make any sense. I couldn’t see any reason why it would work in 32-bit land, but not in 64-bit. Since it was complaining about the “SystemCode” field, I commented out the other fields and played with the field offsets. If I changed the offset from 12 to 16, I could create a SampleHeader object without any runtime errors. Mind you, I could actually use it in my code, those offsets had to match the data my service was receiving.
So I went to plan “B”, getting rid of the explicitly laid out struct. I created a new one without the StructLayout, MarshalAs, and FieldOffset attributes. It looked like this:
public struct SampleHeader
{
public byte[] RecordType;
public uint Version;
public byte[] SystemCode;
public uint LocalID;
public uint HostID;
}
Pretty much the same thing, except .NET defined the field alignments. Instead of using marshalling to copy the data, I just used the BitConverter class. I had already put the received data into a byte[] array, that made it easy to use BitConverter. For this struct, I only needed the LocalID and HostID fields, so the following code was all that I needed:
MyHeader.LocalID = BitConverter.ToUInt32(RawData, 20);
MyHeader.HostID = BitConverter.ToUInt32(RawData, 24);
This replaced the marshalling code that looked like this:
GCHandle handle = GCHandle.Alloc(RawData, GCHandleType.Pinned);
SampleHeader MyHeader = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(SampleHeader));
handle.Free();
I still don’t understand why Windows 64-bit requires fields in a struct to be aligned on 4 byte boundaries, but the replacement code works and is easier to follow.