tags:

views:

963

answers:

4

Youll need a 64bit machine if you want to see the actuall exception. I've created some dummy classes that repro's the problem.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
    public class InnerType
    {
        char make;
        char model;
        UInt16 series;
    }

 [StructLayout(LayoutKind.Explicit)]
    public class OutterType
    {
        [FieldOffset(0)]
        char blah;

        [FieldOffset(1)]
        char blah2;

        [FieldOffset(2)]
        UInt16 blah3;

        [FieldOffset(4)]
        InnerType details;
    }

    class Program
    {
        static void Main(string[] args)
        {
            var t = new OutterType();
            Console.ReadLine();
        }
    }

If I run this on the 64 clr, I receive a type load exception,

System.TypeLoadException was unhandled 
  Message="Could not load type 'Sample.OutterType' from assembly 'Sample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field."

If i force the target cpu to 32, it works fine.

Also, if i change InnerType from a class to a struct it also works. Can someone explain whats going on or what I am doing wrong ?

thanks

A: 

Maybe there goes something Wrong with the Uint16 due to it's not CLS Compliant (see here: http://msdn.microsoft.com/en-us/library/system.uint16.aspx)

Sebastian Sedlak
if i remove the uint16, and decrement the offsets, the issue still occurs.
A: 

If you are wanting to place structs within other structs which are themselves Layoutind.Explict you should Use an explicit Size value (in bytes) if you expect them to work in different bitness modes (or on machines with different packing requirements) What you are saying there is "lay things out sequentially and don't pack internally but use as much space as you like at the end". If you do not specify Size the runtime is free to add as much space as it likes.

The reason it in general refuses to let structs and object types overlap is that the GC routine must be free to traverse the live object graph. While doing this it cannot know if a unioned (overlapping) field is meaningful as an object reference or as raw bits (say an int or a float). Since it must traverse all live object references to behave correctly it would end up traversing 'random' bits which might point anywhere in the heap (or out of it) as if they were references before you know it you're General Protection Faulting.

Since 32/64 references will take up 32 or 64 bits according to the runtime you must use Explict, only union references with references and value types with value types, ensure your reference types are aligned to the boundaries of both target platforms if they differ (Note: Runtime dependent see below) and do one of the following:

  1. Ensure that all reference fields are the last entry in the struct - it is then free to make the struct bigger/smaller depending on the bitness of the runtime environment.
  2. Force all object references to consume 64bits whether you are on a 32 or 64bit environment

Note on alignment: Apologies I was in error on the unaligned reference fields - the compiler removed the type load unless I performed some action with the struct.

[StructLayout(LayoutKind.Explicit)]
public struct Foo
{
    [FieldOffset(0)]
    public byte padding;
    [FieldOffset(1)]
    public string InvalidReference;
}

public static void RunSnippet()
{
    Foo foo;
    foo.padding = 0;
    foo.ValidReference = "blah";
    // Console.WriteLine(foo); // uncomment this to fail
}

The relevant details are in the ECMA specification http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf see section 16.6.2 which mandates the alignment of native size values including &. It notes that the unaligned prefix instruction exists to work round this if required.

On mono however (both OSX intel and Win32 intel 32 bit) the above code works. Either the runtime does not respect the layouts and silently 'correct' things or it allows arbitrary alignment (historically they were less flexible than the MS runtime in this regard which is surprising). The CLI intermediate form generated by mono does not include any .unaligned instruction prefixes, as such it appears to not conform to the spec.

That'll teach me to only check on mono.

ShuggyCoUk
If you run your RunSnippet example you will get the same TypeLoadException as above.
Stephen Martin
+15  A: 

The part about overlapping types is misleading here. The problem is that in .Net reference types must always be aligned on pointer size boundaries. Your union works in x86 since the field offset is 4 bytes which is the pointer size for a 32 bit system but fails on x64 since there it must be offset a multiple of 8. The same thing would happen if you set the offset to 3 or 5 on the x86 platform.

EDIT: For the doubters - I couldn't find a ready reference on the internet but check out Expert .NET 2.0 IL Assembler By Serge Lidin page 175.

Stephen Martin
Great answer. Clear, concise. Perfect.
Aaron
+3  A: 

I have also noticed that you are packing your char data type into 1 byte. Char types in .NET are 2 bytes in size. I cannot verify if this is the actual issue, but I would double-check that.

Brian