Consider the following repro code.
Here, structs S0<S2A> and S0<S2B> are expected to be layed out in memory with the same sequence of fields (even though bool may require more bytes than byte).
unsafe static class Program2
{
[StructLayout(LayoutKind.Explicit, Pack = sizeof(ushort), Size = sizeof(uint))]
struct S1
{
[FieldOffset(0)]
public ushort F1;
[FieldOffset(sizeof(ushort))]
public ushort F2;
}
[StructLayout(LayoutKind.Sequential, Pack = sizeof(ulong))]
unsafe struct S2A { public bool F1; }
[StructLayout(LayoutKind.Sequential, Pack = sizeof(ulong))]
unsafe struct S2B { public byte F1; }
[StructLayout(LayoutKind.Sequential, Pack = sizeof(ulong))]
unsafe struct S0<TS>
where TS : struct
{
public ulong F1;
public S1 SF2;
public ulong F3;
public TS F4;
}
static void Test(ref S0<S2A> instA, ref S0<S2B> instB)
{
instA.F1 = 1;
instA.SF2.F1 = 2;
instA.SF2.F2 = 3;
instA.F3 = 4;
instA.F4.F1 = default;
instB.F1 = 1;
instB.SF2.F1 = 2;
instB.SF2.F2 = 3;
instB.F3 = 4;
instB.F4.F1 = default;
}
static void Main(string[] args)
{
S0<S2A> instA = default;
S0<S2B> instB = default;
Test(ref instA, ref instB);
}
}
Below is actual disasm taken from method Test().
It looks like fields instA.SF2 and instA.F3 were layed out swapped.
--- ...\Program2.cs ---
instA.F1 = 1;
000007FE74C214C0 mov qword ptr [rcx],1
instA.SF2.F1 = 2;
000007FE74C214C7 mov word ptr [rcx+10h],2 <======= unexpected offset
instA.SF2.F2 = 3;
000007FE74C214CD mov word ptr [rcx+12h],3 <======= unexpected offset
instA.F3 = 4;
000007FE74C214D3 mov qword ptr [rcx+8],4 <======= unexpected offset
instA.F4.F1 = default;
000007FE74C214DB mov byte ptr [rcx+18h],0
instB.F1 = 1;
000007FE74C214DF mov qword ptr [rdx],1
instB.SF2.F1 = 2;
000007FE74C214E6 mov word ptr [rdx+8],2
instB.SF2.F2 = 3;
000007FE74C214EC mov word ptr [rdx+0Ah],3
instB.F3 = 4;
000007FE74C214F2 mov qword ptr [rdx+10h],4
instB.F4.F1 = default;
000007FE74C214FA mov byte ptr [rdx+18h],0
000007FE74C214FE ret
--- No source file -------------------------------------------------------------
bool is non-blitable type. The sequential layout is respected only for the unmanaged view of the non-blitable struct (e.g. what the struct is marshalled to if it is used as PInvoke argument). The managed view of non-blittable structs uses auto layout in .NET Core today.
The exact layout used for non-blittable structs is .NET implementation detail. Portable code should not depend on it. It differs between different .NET implementations.
So, LayoutKind.Sequential was silently discarded, right?
Not completely. The sequential layout is still respected for the unmanaged view. If you use this struct as PInvoke argument, sequential layout is going to be respected for that and the bool will be marshalled as 4-byte int.
https://aakinshin.net/blog/post/blittable/ has a nice long explanation of this issue.
Thanks for explanation!
Indeed, trying GCHandle.Alloc() on array of S0<S2A> elements fails with error message telling they're not blittable.
Closing this issue.
Most helpful comment
Not completely. The sequential layout is still respected for the unmanaged view. If you use this struct as PInvoke argument, sequential layout is going to be respected for that and the bool will be marshalled as 4-byte int.
https://aakinshin.net/blog/post/blittable/ has a nice long explanation of this issue.