Search code examples
c#structmarshalling

Overlapping or wrongly aligned struct


I have an issue with my struct layout. I want to have a struct with an array of other structs. The compiler does not complain with my implementation:

[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 8)]
public struct block
{
    [FieldOffset(0)]
    public int x;
    [FieldOffset(4)]
    public int y;
}

[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 84)]
public struct tower
{
    [FieldOffset(0)]
    public int x;
    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public block[] blocks;
};

When I run my program it throws an exception with the following message:

System.TypeLoadException HResult=0x80131522 Message=Could not load type 'tower' from assembly 'F1_Voice, 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. Source=F1_Voice StackTrace: at F1_Voice.Program.UDP() in C:\Users\meule\source\repos\F1_Voice\Program.cs:line 28 at System.Threading.Thread.StartCallback() in /_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs:line 106

I can not figure out what is wrong. I have tried many variations but none have worked so far.

Edit 1:

Okay it works if I change my code to this (if I understood Marc correctly it should be aligned at 8):

[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 88)]
public struct tower
{
    [FieldOffset(0)]
    public int x;
    [FieldOffset(8)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public block[] blocks;
};

But I fail to understand why it works. Since an int is 4 bytes right? so x starting from 0 to 3 and then the array starting from 4 to ....

To answer the question of Marc, I would like it to be a fixed array. Since the data coming in (that will be serialized) always contains the same number of elements.


Solution

  • Fundamentally, the problem here is that public block[] blocks; is not part of the same struct - that field is a reference to an entirely separate array, and does not represent the rest of that same chunk of memory. References need to be word aligned, and on x64 the word size is 8, not 4 - hence the problem.

    However, fixing the alignment to 8 doesn't solve the real problem, as we'd still be talking about unrelated memory, and your deserialize step won't work as intended.


    This is a perfect candidate for the new "inline array" feature in .NET 8; this allows a custom type to look like a regular struct, while actually being refactored (via the runtime and compiler combined) into something that behaves sort of like an array - similar to "fixed size buffers", but for custom types; example:

    using System;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    var obj = new tower();
    for (int i = 0; i < 10; i++)
    {
        obj.Blocks[i] = new Block { x = i, y = -i };
    }
    foreach (var block in obj.Blocks)
    {
        Console.WriteLine($"{block.x} {block.y}");
    }
    
    
    [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 8)]
    public struct Block
    {
        [FieldOffset(0)]
        public int x;
        [FieldOffset(4)]
        public int y;
    }
    
    [InlineArray(10)] // lots of magic happening here
    public struct Block10
    {
        private Block _element0;
    }
    
    [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 84)]
    public struct tower
    {
        [FieldOffset(0)]
        public int x;
        [FieldOffset(4)]
        public Block10 Blocks;
    };
    

    If .NET 8 isn't an option, you can just allocate the space via Size, and wave a magic wand to access the data:

    using System;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    var obj = new tower();
    for (int i = 0; i < 10; i++)
    {
        obj[i] = new Block { x = i, y = -i };
    }
    for (int i = 0; i < 10; i++)
    {
        var block = obj[i];
        Console.WriteLine($"{block.x} {block.y}");
    }
    
    
    [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 8)]
    public struct Block
    {
        [FieldOffset(0)]
        public int x;
        [FieldOffset(4)]
        public int y;
    }
    
    [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 84)]
    public struct tower
    {
        [FieldOffset(0)]
        public int x;
    
        [FieldOffset(4)]
        private Block _block0; // use Unsafe.Add to access the rest
    
        public Block this[int index]
        {
            get
            {
                if (index < 0 || index >= 10) ThrowIndexOutOfRange();
                return Unsafe.Add(ref _block0, index);
            }
            set
            {
                if (index < 0 || index >= 10) ThrowIndexOutOfRange();
                Unsafe.Add(ref _block0, index) = value;
            }
        }
        private static void ThrowIndexOutOfRange()
            => throw new IndexOutOfRangeException("index");
    };