Тема рантайма платформы .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.