When running this code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace StructLayoutTest
{
class Program
{
unsafe static void Main()
{
Console.WriteLine(IntPtr.Size);
Console.WriteLine();
Sequential s = new Sequential();
s.A = 2;
s.B = 3;
s.Bool = true;
s.Long = 6;
s.C.Int32a = 4;
s.C.Int32b = 5;
int* ptr = (int*)&s;
Console.WriteLine(ptr[0]);
Console.WriteLine(ptr[1]);
Console.WriteLine(ptr[2]);
Console.WriteLine(ptr[3]);
Console.WriteLine(ptr[4]);
Console.WriteLine(ptr[5]);
Console.WriteLine(ptr[6]);
Console.WriteLine(ptr[7]); //NB!
Console.WriteLine("Press any key");
Console.ReadKey();
}
[StructLayout(LayoutKind.Explicit)]
struct Explicit
{
[FieldOffset(0)]
public int Int32a;
[FieldOffset(4)]
public int Int32b;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Sequential
{
public int A;
public int B;
public bool Bool;
public long Long;
public Explicit C;
}
}
}
I expect this output BOTH on x86 and x64:
4 or 8 (depending on x86 or x64)
2
3
1
6
0
4
5
garbage
What I get instead on x86:
4
6
0
2
3
1
4
5
garbage
What I get instead on x64:
8
6
0
2
3
1
0
4
5
More:
- The problem goes away when I remove the LayoutKind.Explicit and FieldOffset attributes.
- The problem goes away when I remove the Bool field.
- The problem goes away when I remove the Long field.
- Note that on x64 it seems that the Pack=4 attribute parameter is ignored too?
This applies in .Net3.5 and also .Net4.0
My question: what am I missing? Or is this a bug?
I found a similar question:
Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?
But in my case the layout changes even when the attribute of the substruct changes, without any changes to data types. So it does not look like an optimization. Besides that, I would like to point out that the other question is still unanswered.
In that other question they mention that the layout is respected when using Marshalling. I havent tested that myself but I wonder why is the layout not respected for unsafe code, since all the relevant attributes seem to be in place? Does the documentation mention somewhere that these attributes are ignored unless Marshalling is done? Why?
Considering this, can I even expect LayoutKind.Explicit to work reliably for unsafe code?
Moreover, the documentation mentions the motive of keeping structs with expected layout:
To reduce layout-related problems associated with the Auto value, C#, Visual Basic, and C++ compilers specify Sequential layout for value types.
But this motive apparently does not apply to unsafe code?
From the MSDN Library article for LayoutKind enumeration:
The precise position of each member of an object in unmanaged memory is explicitly controlled, subject to the setting of the StructLayoutAttribute.Pack field. Each member must use the FieldOffsetAttribute to indicate the position of that field within the type.
Relevant phrase highlighted, this is not happening in this program, the pointer is still very much dereferencing managed memory.
And yes, what you are seeing is identical to what happens when a struct contains a member of type DateTime, a type that has [StructLayout(LayoutKind.Auto)] applied. The field marshaller code in the CLR that determines layout makes an effort to honor LayoutKind.Sequential for managed structs as well. But it will quickly give up without a squeal if it encounters any member that conflicts with this goal. A struct that itself is not sequential is sufficient for that. You can see this being done in the SSCLI20 source, src/clr/vm/fieldmarshaler.cpp, search for fDisqualifyFromManagedSequential
Which will make it switch to automatic layout, the same layout rule that's applied to classes. It rearranges fields to minimize the padding between members. With the net effect that the amount of memory required is smaller. There are 7 bytes of padding after the "Bool" member, unused space to get the "Long" member aligned to an address that's a multiple of 8. Very wasteful of course, it fixes that by making the long the first member in the layout.
So instead of the explicit layout with /* offset - size */ annotated:
public int A; /* 0 - 4 */
public int B; /* 4 - 4 */
public bool Bool; /* 8 - 1 */
// padding /* 9 - 7 */
public long Long; /* 16 - 8 */
public Explicit C; /* 24 - 8 */
/* Total: 32 */
It comes up with:
public long Long; /* 0 - 8 */
public int A; /* 8 - 4 */
public int B; /* 12 - 4 */
public bool Bool; /* 16 - 1 */
// padding /* 17 - 3 */
public Explicit C; /* 20 - 8 */
/* Total: 28 */
With an easy 4 bytes of memory saved. The 64-bit layout requires additional padding to ensure that the long is still aligned when it is stored in an array. This is all highly undocumented and subject to change, be sure to never take a dependency on managed memory layout. Only Marshal.StructureToPtr() can give you a guarantee.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With