Тема рантайма платформы .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;
}
}
Verification evasion
Рассмотрим простое приложение:
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.