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<int, int>)Delegate.CreateDelegate(                                                         typeof(Func<int, int>),                                                          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<Calc, int, int>)Delegate.CreateDelegate(                                                         typeof(Func<Calc, int, int>),                                                          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<Calc,int,int> аргумент this byref, но как?!

Объявим свой делегат FuncByRef

delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2); 

Изменим код:

class Program {     delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2);      static void Main(string[] args)     {         var addMethodInfo = typeof(Calc).GetMethod("Add",                                              BindingFlags.Public | BindingFlags.Instance);          var addDelegate = (FuncByRef<Calc, int, int>)Delegate.CreateDelegate(                                                         typeof(FuncByRef<Calc, int, int>),                                                         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

Далее 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.

ссылка на оригинал статьи http://habrahabr.ru/post/248775/


Комментарии

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

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