Friday, October 17, 2008

Strange 64-bit error with LayoutKind.Explicit

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.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.