Hello. I would like to share an example of using StructLayout for something more interesting than examples with bytes, inta, and other numbers, in which everything happens a little more than expected.
Before proceeding to lightning-fast violation of encapsulation, it is worth recalling briefly what StructLayout is. Strictly speaking, it is even a StructLayoutAttribute, that is, an attribute that allows you to create structures and classes similar to a union in C ++. If we talk in more detail, this attribute allows you to take control of the placement of class members in memory for themselves. Accordingly, it is placed above the class. Usually, if a class has 2 fields, we expect them to be arranged sequentially, that is, they will be independent of each other (do not overlap). However, StructLayout allows you to specify that the location of the fields will be set not by the environment, but by the user. To explicitly specify the field offset, use the LayoutKind.Explicit parameter. To indicate which offset from the beginning of the class / structure (hereinafter the class) we want to place the field, we need to put the FieldOffset attribute on it, which takes as a parameter the number of bytes - the distance from the beginning of the class. It is impossible to transfer a negative value, so it’s going to be a bit more complicated to spoil the pointers to the method table or the index of the synchronization block.
Let's start writing the code. To begin with, I suggest starting with a simple example. Create a class of the following form:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); }
Next, we use the above described mechanism for explicitly specifying field offsets.
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; }
For now, I'll postpone the explanations and use the written class as follows:
class Program { static void Main(string[] args) { CustomStructWithLayout instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClass(); instance.Str = "4564"; Console.WriteLine(instance.SomeInstance.GetType()); //System.String Console.WriteLine(instance.SomeInstance.ToString()); //4564 Console.Read(); } }
Total Calling the GetType () method returns a string, the ToString () method plays pranks and gives us the string "4564".
Brain Discharge: What will be displayed when calling the CustomClass virtual property?
As you already guessed, we initialized CustomStructWithLayout, both links are null, then we initialize the field of our type, and then we assign the string to the Str field. As a result, a little more remains from CustomClass than nothing. Over it was written a string with its entire internal structure, including the table of methods and the index of the synchronization unit. But the compiler sees the field is still of the type of our class.
For proof, here is a small clipping from WinDbg:

Here you can see some unusual things. The first is that in the object of the address on the method tables, the class fields are different, as expected, but the address of the field value is one. The second is that you can see that both fields are located at offset 4. I think most will understand, but just in case I will explain, the link to the method table is located directly at the address of the object. The fields begin with an offset of 4 bytes (for 32bit bits), and the index of the synchronization unit is located at an offset of -4.
')
Now that you have figured out what is happening, you can try using offsets to call what should not have been called.
For this, I repeated the structure of the string class in one of my classes. True, I repeated only the beginning, since the class string is quite voluminous.
public class CustomClassLikeString { public const int FakeAlignConst = 3; public const int FakeCharPtrAlignConst = 3; public static readonly object FakeStringEmpty; public char FakeFirstChar; public int FakeLength = 3; public const int FakeTrimBoth = 3; public const int FakeTrimHead = 3; public const int FakeTrimTail = 3; public CustomClassLikeString(){} public CustomClassLikeString(int a){} public CustomClassLikeString(byte a){} public CustomClassLikeString(short a){} public CustomClassLikeString(string a){} public CustomClassLikeString(uint a){} public CustomClassLikeString(ushort a){} public CustomClassLikeString(long a){ } public void Stub1(){} public virtual int CompareTo(object value) { return 800; } public virtual int CompareTo(string value) { return 801; } }
Well, the structure is changing a bit with the layout
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; }
Further, when calling FakeLength or the CompareTo () method, due to the identical offset of these class members relative to the address of the object itself, the corresponding string method will be called (in this case). Getting to the first private method in a row that I can use was quite long, so I stopped at a public one. But the field is private, everything is honest. By the way, the methods are made virtual to protect against any optimizations that interfere with the work (for example, embedding), and also so that the method is called by the offset in the table of methods.
So, performance. It’sa matter that a direct competitor in calling what isn’t necessary and in violation of encapsulation is reflection. I think that it is clear that we are faster than this thing, all of us do not analyze metadata. Exact values:
Method | Job job | Mean | Error | Stddev | Median |
---|
StructLayoutField | Clr | 0.0597 ns | 0.0344 ns | 0.0396 ns | 0.0498 ns |
Reflectionfield | Clr | 197.1257 ns | 1.9148 ns | 1.7911 ns | 197.4787 ns |
StructLayoutMethod | Clr | 3.5195 ns | 0.0382 ns | 0.0319 ns | 3.5285 ns |
ReflectionMethod | Clr | 743.9793 ns | 13.7378 ns | 12.8504 ns | 743.8471 ns |
Here is a long piece of code with how I measured performance (If someone needs it):
Code [ClrJob] [RPlotExporter, RankColumn] [InProcessAttribute] public class Benchmarking { private CustomStructWithLayout instance; private string str; [GlobalSetup] public void Setup() { instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClassLikeString(); instance.Str = "4564"; str = "4564"; } [Benchmark] public int StructLayoutField() { return instance.SomeInstance.FakeLength; } [Benchmark] public int ReflectionField() { return (int)typeof(string).GetField("m_stringLength", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(str); } [Benchmark] public int StructLayoutMethod() { return instance.SomeInstance.CompareTo("4564"); } [Benchmark] public int ReflectionMethod() { return (int)typeof(string).GetMethod("CompareTo", new[] { typeof(string) }).Invoke(str, new[] { "4564" }); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<Benchmarking>(); } }