Hi. I would like to show you an example of using StructLayout for something more interesting than examples with bytes, ints, and other primitive types, when everything happens quite obviously.



Before proceeding to lightning-fast violation of encapsulation, it is worth recalling briefly what StructLayout is. Strictly speaking, it is even StructLayoutAttribute, an attribute that allows you to create structures and classes similar to union in C ++. This attribute allows you to take control of the placement of class members in memory (using offsets). 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 specify the offset of the fields explicitly we should use the parameter LayoutKind.Explicit.

To indicate which offset from the beginning of the class / structure (hereinafter just «class») we want to place the field, we need to put the FieldOffset attribute on it. This attribute takes as a parameter the number of bytes — the offset from the beginning of the class. It is impossible to pass a negative value, in order not to spoil the pointers to the method table or the sync block index. So it’s going to be a little more complicated.

Let's start writing the code. To begin with, I suggest starting with a simple example. Create following class:

    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();
        }
    }

Calling the GetType() method returns a string, the ToString() method is naughty and gives us the string «4564».

Brain Discharge: What will be displayed after 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 assign the string to the Str field. As a result, CustomClass link doesn't points to CustomClass object, it points to the System.string object (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:



Здесь можно увидеть несколько необычных вещей.

  • In CustomStructWithLayout object fields have different addresses of methodtables (highly expected), but the addresses of objects are the same.
  • 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, directly to the address of the object placed a link to the table of methods. The fields begin with an offset of 4 bytes (for 32 bits), and the index of the synchronization block is located with an offset of -4. Thus, both objects are at the same offset.

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. But I repeated only the beginning, since the class string is quite voluminous and I am very lazy.

Note: consts and statics are not required, just for fun.

    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 with the layout will be changed a bit.

    [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 of the string that I can use was too 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 method table.

So, performance. It is clear that a direct competitor in calling things that should not be called and in violation of encapsulation is reflection. I think that it is clear that we are faster than this thing, because we do not analyze metadata. Exact values:
Method 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>();
        }
    }


Russian version

Комментарии (0)