Age of JIT compiling. Part I. Genesis

Тема рантайма платформы .NET освещена весьма подробно. Однако работа самого JIT, результирующий код и взаимодействие со средой исполнения — не очень.Ну что ж, исправим это!

Узнаем причины отсутствия наследования у структур, природу unbound delegates.

А еще… вызов любых методов у любых объектов без reflection.

▌Genesis of Value-typesСтруктуры в .NET являются с одной стороны структурами в классическом понимании данного слова (layout, mutability и т.д.), с другой стороны имеют поддержку ООП и среды .NET в принципе (методы ToString, GetHashCode; наследование от System.ValueType, который в свою очередь от System.Object; и т.д.).Чтобы лучше понять почему структуры нельзя наследовать от других типов, необходимо перейти на уровень организации методов в CLR.

Instance-level методы имеют неявный аргумент this. На самом деле он явный. JIT, компилируя код, создает сигнатуру следующего вида:

ReturnType MethodName (Type this, …arguments…) Но это для ссылочных типов.Для значимых:

ReturnType MethodName (ref Type this, …arguments…) Да-да! Сделано это для поддержки изменяемости структур, т.е. чтобы мы могли модифицировать this.Так почему же нельзя наследовать структуры от других типов?

Ответим на вопрос:, а если это будет виртуальный метод базового ссылочного класса? Как быть JIT-компилятору? Никак. Постоянно угадывать и генерировать различные специализации кода (с семантикой byval и byref), кроме еще и диспетчеризации таблицы виртуальных методов — неэффективно. Добавляется и boxing, чтобы правильно обслужить виртуальный метод.

Но… Методы ToString, GetHashCode, Equals являются виртуальными методами ссылочного класса-предка System.Object?!

Это исключения. JIT знает об этом и генерирует привязку и специализацию только для этих методов.

▌Unbound Delegates Reflection в .NET позволяет нам создать делегат как на статические методы, так и на экземпляров.Однако есть небольшая проблема — для экземпляров необходимо создавать делегату по новому.Рассмотрим пример: class Program { static void Main (string[] args) { var calc = new Calc () { FirstOperand = 2 };

var addMethodInfo = typeof (Calc).GetMethod («Add», BindingFlags.Public | BindingFlags.Instance);

var addDelegate = (Func)Delegate.CreateDelegate ( typeof (Func), calc, addMethodInfo);

Console.WriteLine (addDelegate (2)); // 4 } }

class Calc { public int FirstOperand = 0;

public int Add (int secondOperand) { return FirstOperand + secondOperand; } } На помощь приходят unbound delegates, т.е. непривязанные. Однако у них есть одна особенность: иная сигнатура, где добавляется (да, Вы правильно догадались) первый аргумент — ссылка на экземпляр.Т.е. unbound delegates — это и есть ссылки на «реальный» метод.

Так, сигнатура Add (int secondOperand) превратиться в Add (Calc this, int secondOperand).

Проверим: class Program { static void Main (string[] args) { var addMethodInfo = typeof (Calc).GetMethod («Add», BindingFlags.Public | BindingFlags.Instance);

var addDelegate = (Func)Delegate.CreateDelegate ( typeof (Func), null, addMethodInfo);

Console.WriteLine (addDelegate (new Calc (), 2)); // 2 } }

class Calc { public int FirstOperand = 0;

public int Add (int secondOperand) { return FirstOperand + secondOperand; } } Помните вопрос про сигнатуры методов структур? Объявите тип Calc как struct и запустите. ArgumentException? Да? Нам нужно передать в Func аргумент this byref, но как?!

Объявим свой делегат FuncByRef delegate TResult FuncByRef(ref T1 arg1, T2 arg2); Изменим код: class Program { delegate TResult FuncByRef(ref T1 arg1, T2 arg2);

static void Main (string[] args) { var addMethodInfo = typeof (Calc).GetMethod («Add», BindingFlags.Public | BindingFlags.Instance);

var addDelegate = (FuncByRef)Delegate.CreateDelegate ( typeof (FuncByRef), null, addMethodInfo); var calc = new Calc (); calc.FirstOperand = 123;

Console.WriteLine (addDelegate (ref calc, 2)); // 125 } }

struct Calc { public int FirstOperand;

public int Add (int secondOperand) { return FirstOperand + secondOperand; } } ▌Unbound Delegates Рассмотрим простое приложение: class Program { static void Main (string[] args) { CallTest (new object ()); CallTestWithExlicitCasting (new object ()); Console.Read (); }

static void CallTest (object target) { Program p = target as Program; p.Test (); }

static void CallTestWithExlicitCasting (object target) { Program p = (Program)target; p.Test (); }

public void Test () { Console.WriteLine («Test»); } } Как можно заметить, приложение упадет с NullReferenceException при вызове CallTest ().Что ж, исправим данную ситуацию. Для этого запустим ildasm.

Visual Studio Command Promt → ildasm

40debe14a1264308bfc616337d82fea9.png

Далее File → Dump → Save as dialog → msiltricks_patch.il

Открываем сохраненный файл msiltricks_patch.il в любимом редакторе и на ходим тело метода CallTest:

.method private hidebysig static void CallTest (object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: isinst MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program: Test () IL_000d: ret } // end of method Program: CallTest Удалим сроку IL_0001: isinst MSILTricks.Program, т.е. вызов оп-кода isinst (он же оператор as в C#).Проделываем то же самое и с методом CallTestWithExlicitCasting:

.method private hidebysig static void CallTestWithExlicitCasting (object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: castclass MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program: Test () IL_000d: ret } // end of method Program: CallTestWithExlicitCasting Удалим сроку IL_0001: castclass MSILTricks.Program, т.е. вызов оп-кода castclass (он же оператор явного приведения в C#).Visual Studio Command Promt → cd [your saved file dir]Visual Studio Command Promt → ilasm msiltricks_patch.il

Запустим msiltricks_patch.exe

Ни одного исключения, даже AccessViolationException.Ха-ха!

Дело в том, что наш метод Test не имеет побочных эффектов, а также не использует this в своем теле.

Вывод: мы с Вами работаем с «железом» и переменные ссылочных типов являются просто адресами в памяти, т.е. DWORD; приведение типов и т.д. являются не более чем абстракцией и «защитой» на этапе компиляции. Центральный процессор работает именно с адресами в памяти. CLR предоставляет эти адреса, JIT компилирует код, учитывая их.

Ваш КО :)

И, да, инструкция callvirt не проверяет на «правильность» объекта.Чтобы получить AccessViolationException, можно добавить, например, виртуальный метод в класс Program и вызвать его в методе Test.

© Habrahabr.ru