Всем привет. Хотелось бы поделиться примером использования StructLayout для чего-то более интересного, чем примеры с байтами, интами и прочими цифрами, в которых все происходит чуть более, чем ожидаемо.
Прежде, чем приступить к молниеносному нарушению инкапсуляции, стоит в двух словах напомнить, что такое StructLayout. Строго говоря, это даже StructLayoutAttribute, то бишь атрибут, который позволяет создавать структуры и классы, подобные union в С++. Если говорить еще более подробно, то данный атрибут позволяет взять управление размещением членов класса в памяти на себя. Соответсвенно, ставится он над классом. Обычно, если класс имеет 2 поля, мы ожидаем, что они будут располагаться последовательно, то бишь будут независимы друг от друга (не перекрывать). Однако, StructLayout дает возможность указать, что расположение полей будет задавать не среда, а пользователь. Для явного указания смещения полей следует использовать параметр LayoutKind.Explicit. Для указания, по какому смещению относительно начала класса/структуры (в дальнейшем класса) мы хотим разместить поле, над ним следует поставить атрибут FieldOffset, который принимает в качестве параметра количесво байт — отступ от начала класса. Отрицательное значение передать не получится, так что о том, чтобы испортить указатели на таблицу методов или индекс блока синхронизации, даже и не думайте, все будет немного сложнее.
Приступим к написанию кода. Для начала предлагаю начать с простого примера. Создадим класс следующего вида:
public class CustomClass
{
public override string ToString()
{
return "CUSTOM";
}
public virtual object Field { get; } = new object();
}
Далее используем вышеописанный механизм явного задания смещений полей.
[StructLayout(LayoutKind.Explicit)]
public class CustomStructWithLayout
{
[FieldOffset(0)]
public string Str;
[FieldOffset(0)]
public CustomClass SomeInstance;
}
Пока отложу объяснения и воспользуюсь написанным классом следующим образом:
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();
}
}
Итого. Вызов метода GetType() выдает string, метод ToString() шалит и дает нам строку «4564».
Разрядка для мозгов: Что будет выведено при вызове виртуального свойства CustomClass?
Как вы уже догадались, мы проинициализировали CustomStructWithLayout, обе ссылки равны null, далее инициализируем поле нашего типа, а после присваиваем строку полю Str. В итоге от CustomClass остается чуть больше, чем ничего. Поверх его была записана строка со всей ее внутренней структурой, включая таблицу методов и индекс блока синхронизации. Но компилятор видит поле все еще типа нашего класса.
Для доказательсва приведу небольшую вырезку из WinDbg:
Здесь можно увидеть несколько необычных вещей. Первая — в объекте адреса на таблицы методов у полей класса разные, что и ожидаемо, но адрес значения поля один. Вторая — можно увидеть, что оба поля расположены по смещению 4. Думаю, большинсво поймет, но на всякий случай поясню, непосредсвенно по адресу объекта располагается ссылка на таблицу методов. Поля начинаются со смещением 4 байта (длz 32 бит), а индекс блока синхронизации расположен со смещением -4.
Теперь, когда разобрались, что происходит, можно попробовать использовать смещения для вызова того, что вызывать не следовало бы.
Для этого я повторил структуру класса string в одном из своих классов. Правда повторил я лишь начало, поскольку класс string весьма объемный.
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;
}
}
Ну и немного меняется структура с Layout
[StructLayout(LayoutKind.Explicit)]
public class CustomStructWithLayout
{
[FieldOffset(0)]
public string Str;
[FieldOffset(0)]
public CustomClassLikeString SomeInstance;
}
Далее, при вызове FakeLength или метода CompareTo() благодаря идентичным смещением этих членов класса относительно адреса самого объекта будет вызван соответсвующий метод строки (в данном случае). Добираться до первого приватного метода в строке, который я могу использовать, было довольно долго, поэтому я остановился на публичном. Но поле приватное, все честно. Кстати, методы сделаны виртуальными для защиты от всяких оптимизаций, мешающий работе (например, встраивания), а также для того, чтоб метод вызывался по смещению в таблице методов.
Итак, производительность. Исное дело, что прямой конкурент в вызове того, что вызвать не надо и в нарушении инкапсуляции — рефлексия. Я думаю, что и так понятно, что мы быстрее этой вещи, все ж мы не анализируем метаданные. Точные значения:
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 |
Здесь длинный кусок кода с тем, как я измерял производительность (Если кому-то оно надо):
[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>();
}
}
Автор: ZloyChert