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
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
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
Объявим свой делегат FuncByRef
delegate TResult FuncByRef
static void Main (string[] args) { var addMethodInfo = typeof (Calc).GetMethod («Add», BindingFlags.Public | BindingFlags.Instance);
var addDelegate = (FuncByRef
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
Далее 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.