Хабрахабр

[Из песочницы] Не уважаю инкапсуляцию, или использование таблицы методов другого типа для быстрого вызова приватных методов

Всем привет. Хотелось бы поделиться примером использования StructLayout для чего-то более интересного, чем примеры с байтами, интами и прочими цифрами, в которых все происходит чуть более, чем ожидаемо.
Прежде, чем приступить к молниеносному нарушению инкапсуляции, стоит в двух словах напомнить, что такое StructLayout. Строго говоря, это даже StructLayoutAttribute, то бишь атрибут, который позволяет создавать структуры и классы, подобные union в С++. Если говорить еще более подробно, то данный атрибут позволяет взять управление размещением членов класса в памяти на себя. Соответсвенно, ставится он над классом. Обычно, если класс имеет 2 поля, мы ожидаем, что они будут располагаться последовательно, то бишь будут независимы друг от друга (не перекрывать). Однако, StructLayout дает возможность указать, что расположение полей будет задавать не среда, а пользователь. Для явного указания смещения полей следует использовать параметр LayoutKind.Explicit. Для указания, по какому смещению относительно начала класса/структуры (в дальнейшем класса) мы хотим разместить поле, над ним следует поставить атрибут FieldOffset, который принимает в качестве параметра количесво байт — отступ от начала класса. Отрицательное значение передать не получится, так что о том, чтобы испортить указатели на таблицу методов или индекс блока синхронизации, даже и не думайте, все будет немного сложнее.

Для начала предлагаю начать с простого примера. Приступим к написанию кода. Создадим класс следующего вида:

public class CustomClass 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?

В итоге от CustomClass остается чуть больше, чем ничего. Как вы уже догадались, мы проинициализировали CustomStructWithLayout, обе ссылки равны null, далее инициализируем поле нашего типа, а после присваиваем строку полю Str. Но компилятор видит поле все еще типа нашего класса.
Для доказательсва приведу небольшую вырезку из 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>(); } }

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть