Разработка → Быстрая и удобная генерация IL

из песочницы
homuroll 15 июля 2015 в 17:53 12,2k
Я много раз сталкивался с задачей динамической генерации кода (например, при написании эффективного сериализатора или компилятора DSL). Это можно делать разными способами, какой из них лучший – дискуссия для отдельной статьи. По ряду причин я предпочитаю Reflection.Emit и CIL (Common Intermediate Language) и расскажу, с какими проблемами пришлось столкнуться на этом пути, а также об их решении: умной обертке над ILGeneratorGroboIL из библиотеки Graceful Emit.

Хочу отметить при этом, что иногда встречаются ситуации, когда у нас нет большого выбора: например, при написании сериализатора необходимо иметь доступ к приватным полям, и приходится использовать IL. Кстати, известный сериализатор protobuf-net содержит несколько сотен IL-инструкций.

Если вы ни разу не сталкивались с использованием IL-кода, то статья может показаться сложной для понимания, поскольку содержит много примеров кода с использованием IL. Для получения базовых знаний рекомендую прочитать статью Introduction to IL Assembly Language.

Reflection.Emit предоставляет два способа генерации кода – DynamicMethod и TypeBuilder/MethodBuilder.

DynamicMethod – это «легковесный» статический метод, результатом компиляции которого будет делегат. Основное их преимущество в том, что DynamicMethod'ам разрешается игнорировать видимость типов и членов типов. Они собираются сборщиком мусора, когда все ссылки на них будут сброшены, но с .NET Framework 4.0 такая возможность появилась и у DynamicAssembly, так что это уже не является преимуществом.

С помощью DynamicAssembly/ModuleBuilder/TypeBuilder/MethodBuilder можно динамически генерировать все пространство типов .NET: интерфейсы, классы, переопределять виртуальные методы, объявлять поля, свойства, реализовывать конструкторы и т. д. То есть это будет обычная assembly, которую можно даже сохранить на диск.

На практике чаще используются DynamicMethod'ы, поскольку они несколько проще в объявлении и имеют доступ к приватным членам. MethodBuilder'ы обычно используются, если помимо кода есть необходимость сгенерировать какие-то данные: тогда их удобно поместить в TypeBuilder'ы, а код – в их методы.

Пример


Задача: напечатать все поля объекта.

public static Action<T> BuildFieldsPrinter<T>() where T : class
{
   var type = typeof(T);
   var method = new DynamicMethod(Guid.NewGuid().ToString(), // имя метода
                                  typeof(void), // возвращаемый тип
                                  new[] {type}, // принимаемые параметры
                                  typeof(string), // к какому типу привязать метод, можно указывать, например, string
                                  true); // просим доступ к приватным полям
   var il = method.GetILGenerator();
   var fieldValue = il.DeclareLocal(typeof(object));
   var toStringMethod = typeof(object).GetMethod("ToString");
   var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
   foreach(var field in fields)
   {
       il.Emit(OpCodes.Ldstr, field.Name + ": {0}"); // stack: [format]
       il.Emit(OpCodes.Ldarg_0); // stack: [format, obj]
       il.Emit(OpCodes.Ldfld, field); // stack: [format, obj.field]
       if(field.FieldType.IsValueType)
            il.Emit(OpCodes.Box, field.FieldType); // stack: [format, (object)obj.field]
       il.Emit(OpCodes.Dup); // stack: [format, obj.field, obj.field]
       il.Emit(OpCodes.Stloc, fieldValue); // fieldValue = obj.field; stack: [format, obj.field]
       var notNullLabel = il.DefineLabel();
       il.Emit(OpCodes.Brtrue, notNullLabel); // if(obj.field != null) goto notNull; stack: [format]
       il.Emit(OpCodes.Ldstr, "null"); // stack: [format, "null"]
       var printedLabel = il.DefineLabel();
       il.Emit(OpCodes.Br, printedLabel); // goto printed
       il.MarkLabel(notNullLabel);
       il.Emit(OpCodes.Ldloc, fieldValue); // stack: [format, obj.field]
       il.EmitCall(OpCodes.Callvirt, toStringMethod, null); // stack: [format, obj.field.ToString()]
       il.MarkLabel(printedLabel);
       var writeLineMethod = typeof(Console).GetMethod("WriteLine", new[] { typeof(string), typeof(object) });
       il.EmitCall(OpCodes.Call, writeLineMethod, null); // Console.WriteLine(format, obj.field.ToString()); stack: []
   }
   il.Emit(OpCodes.Ret);
   return (Action<T>)method.CreateDelegate(typeof(Action<T>));
}


Проблемы ILGenerator


Начнем с того, что у ILGenerator'а плохой синтаксис: есть один метод Emit с кучей перегрузок, поэтому легко по ошибке вызвать неправильную перегрузку.

Также неудобно, что у одной логической IL-инструкции может быть несколько вариантов, например, у инструкции ldelem есть 11 вариантов – ldelem.i1 (sbyte), ldelem.i2 (short), ldelem.i4 (int), ldelem.i8 (long), ldelem.u1 (byte), ldelem.u2 (ushort), ldelem.u4 (uint), ldelem.r4 (float), ldelem.r8 (double), ldelem.i (native int), ldelem.ref (reference type).

Но это все семечки по сравнению с тем, насколько плохо выдаются сообщения об ошибках.

Во-первых, исключение вылетает только в самом конце, при попытке компиляции метода JIT-компилятором (то есть даже не на вызове DynamicMethod.CreateDelegate() или TypeBuilder.CreateType(), а при первой попытке реального запуска этого кода), поэтому не понятно, какая именно инструкция вызвала ошибку.

Во-вторых, сами сообщения об ошибках, как правило, ни о чем не говорят, к примеру, самая частая ошибка – «Common language runtime detected an invalid program».

Примеры ошибок/опечаток



  1. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    il.Emit(OpCodes.Ldfld); // Пытаемся загрузить поле, но забыли передать FieldInfo
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    InvalidProgramException: «Common language runtime detected an invalid program».


  2. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    il.Emit(OpCodes.Box); // Хотели скастовать value type к object, но забыли передать тип
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    InvalidProgramException: «Common language runtime detected an invalid program».


  3. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    var code = GetCode(..); // Функция возвращает byte
    il.Emit(OpCodes.Ldc_I4, code); // Хотели загрузить константу типа int, но передали byte
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    InvalidProgramException: «Common language runtime detected an invalid program».


  4. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    il.Emit(OpCodes.Call, abstractMethod); // Хотели вызвать абстрактный метод, но случайно вместо Callvirt написали Call
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    BadImageFormatException: «Invalid il format».


  5. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    var keyGetter = typeof(KeyValuePair<int, int>).GetProperty("Key").GetGetMethod();
    il.Emit(OpCodes.Ldarg_1); // Аргумент 1 – KeyValuePair<int, int>
    il.Emit(OpCodes.Call, keyGetter); // Хотели взять свойство Key у KeyValuePair<int, int>, но это value type,
                                      // поэтому его нужно загружать по адресу, чтобы вызвать метод
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    InvalidProgramException: «Common language runtime detected an invalid program».


  6. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    var toStringMethod = typeof(object).GetMethod("ToString");
    il.Emit(OpCodes.Ldarga, 1); // Аргумент 1 – int, загрузили по адресу
    il.Emit(OpCodes.Callvirt, toStringMethod); // Хотели вызвать int.ToString(), но для вызова виртуального метода
                                               // на value type по адресу нужен префикс constrained
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    NullReferenceException: «Object reference not set to instance of an object».
    Или
    AccessViolationException: «Attempted to read or write protected memory. This is often an indication that other memory is corrupt».


  7. var il = dynamicMethod.GetILGenerator();
    {..} // Здесь какие-то инструкции
    var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; // Хотим достать приватное поле value
    var valueField = typeof(KeyValuePair<int, string>).GetField("value", bindingFlags);
    il.Emit(OpCodes.Ldarga, 1); // Аргумент 1 – KeyValuePair<string, int>
    il.Emit(OpCodes.Ldfld, valueField); // Хотели взять поле value у KeyValuePair<string, int>, но случайно вместо
                                        // KeyValuePair<string, int> написали KeyValuePair<int, string>, в итоге
                                        // возьмем поле key типа int и проинтерпретируем его как string
    {..} // Здесь какие-то инструкции
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    var result = compiledMethod(..);  // ← Здесь не будет исключения
    {..} // Какая-то работа с result ← Будет исключение
    

    Неопределенное поведение, скорее всего, будет AccessViolationException или NullReferenceException.

  8. Забыли в конце кода вызвать инструкцию OpCodes.Ret – получим неопределенное поведение: может, вылетит исключение при попытке компиляции, может просто все сломаться уже во время работы, а может повезти и все будет работать правильно.

  9. Реализуем функцию
    static int Add(int x, double y) { return x + (int)y; }

    var il = dynamicMethod.GetILGenerator();
    	il.Emit(OpCodes.Ldarg_0); // Аргумент 0 - типа int
    	il.Emit(OpCodes.Ldarg_1); // Аргумент 1 - типа double
    	il.Emit(OpCodes.Add); // Забыли сконвертировать double к int. Непонятно что будет
    	il.Emit(OpCodes.Ret);
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    var result = compiledMethod(..);  // ← Здесь может не быть исключения
    

    В спецификации CIL сказано, что инструкция OpCodes.Add не может принимать аргументы типов int и double, но исключения может не быть, просто будет неопределенное поведение, зависящее от JIT-компилятора.

    Пример запуска:
    • x64: compiledMethod(10, 3.14) = 13
      ASM-код (x лежит в ecx, y — в xmm1):
      cvtsi2sd xmm0, ecx
      addsd xmm0, xmm1
      cvttsd2si eax, xmm0
    • x86: compiledMethod(10, 3.14) = 20
      ASM-код (x лежит в ecx, y — на стэке):
      mov eax, ecx
      fld qword [esp + 4]
      add eax, ecx
      fstp st(0)

    То есть под x64 сгенерировалась наиболее логичная интерпретация (int конвертируется к double, потом два double складываются и результат обрезается до int), а вот под x86 попытка смешения целочисленных и вещественных операндов привела к тому, что вместо x + y возвращается 2 * x (читателям предлагаю посмотреть, что будет, если вместо int + double написать double + int).

  10. Реализуем функцию
    static string Coalesce(string str) { return str ?? ""; }

    var il = dynamicMethod.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0); // stack: [str]
    il.Emit(OpCodes.Dup); // stack: [str, str]
    var notNullLabel = il.DefineLabel();
    il.Emit(OpCodes.Brtrue, notNullLabel); // if(str != null) goto notNull; stack: [str]
    il.Emit(OpCodes.Ldstr, ""); // Oops, забыли, что на стэке еще осталось значение str
    il.MarkLabel(notNullLabel); // В этом месте у нас неконсистентный стэк: в нем либо одно значение, либо два
    il.Emit(OpCodes.Ret);
    var compiledMethod = dynamicMethod.CreateDelegate(..);
    compiledMethod(..);  // ← Здесь вылетит исключение
    

    InvalidProgramException: «JIT compiler encountered an internal limitation».

    Сюда же подпадает большое количество похожих ошибок: забыли положить this для вызова instance-метода, забыли положить аргумент метода, положили не то значение аргумента метода и т. д.

Если текст функции состоит из десятка инструкций, то еще можно как-то, перечитав код несколько раз, понять, в чем же ошибка, но если код состоит из сотен команд, то разработка такого кода становится очень муторным и долгим занятием.
Если же все же удается заставить такой код скомпилироваться, то дебагать его невозможно. Единственное, что можно сделать, это помимо кода сгенерировать еще символьную информацию, но это долго, неудобно и сложно поддерживать в актуальном состоянии.

Поэтому, имея достаточно большой опыт написания IL-кода с помощью ILGenerator и порядком измучившись, я решил написать свой, учтя все проблемы, на которые я наталкивался.
Задача была написать такой IL-генератор, чтобы исключение InvalidProgramException вообще никогда бы не вылетало, а подхватывалось где-то раньше с понятным текстом ошибки.

GroboIL


Результатом стал GroboIL – умная обертка над ILGenerator.

Особенности GroboIL:
  • Более удобный синтаксис: на каждую инструкцию по одной функции, все похожие инструкции объединены вместе, например, вместо 11 инструкций OpCodes.Ldelem_* есть один метод GroboIL.Ldelem(Type type).
  • Во время генерации кода GroboIL формирует содержимое стэка вычислений и валидирует аргументы инструкций, и если что-то пошло не так, то тут же кидает исключение.
  • Есть дебаг-вывод генерируемого кода.
  • Есть возможность дебага MethodBuilder'ов.
  • Приемлемая производительность. Например, как-то мне пришлось столкнуться с функцией из 500 000 инструкций, и обработка заняла 3 секунды (при этом компиляция метода JIT-компилятором заняла 84 секунды и отъела 4ГБ памяти).


Предыдущий пример, переписанный с использованием GroboIL:

public static Action<T> BuildFieldsPrinter<T>() where T : class
{
   var type = typeof(T);
   var method = new DynamicMethod(Guid.NewGuid().ToString(), // имя метода
                                  typeof(void), // возвращаемый тип
                                  new[] { type }, // принимаемые параметры
                                  typeof(string), // к какому типу привязать метод, можно указывать, например, string
                                  true); // просим доступ к приватным полям
   using(var il = new GroboIL(method))
   {
       var fieldValue = il.DeclareLocal(typeof(object), "fieldValue");
       var toStringMethod = typeof(object).GetMethod("ToString");
       var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
       foreach(var field in fields)
       {
           il.Ldstr(field.Name + ": {0}"); // stack: [format]
           il.Ldarg(0); // stack: [format, obj]
           il.Ldfld(field); // stack: [format, obj.field]
           if(field.FieldType.IsValueType)
               il.Box(field.FieldType); // stack: [format, (object)obj.field]
           il.Dup(); // stack: [format, obj.field, obj.field]
           il.Stloc(fieldValue); // fieldValue = obj.field; stack: [format, obj.field]
           var notNullLabel = il.DefineLabel("notNull");
           il.Brtrue(notNullLabel); // if(obj.field != null) goto notNull; stack: [format]
           il.Ldstr("null"); // stack: [format, "null"]
           var printedLabel = il.DefineLabel("printed");
           il.Br(printedLabel); // goto printed
           il.MarkLabel(notNullLabel);
           il.Ldloc(fieldValue); // stack: [format, obj.field]
           il.Call(toStringMethod); // stack: [format, obj.field.ToString()]
           il.MarkLabel(printedLabel);
           var writeLineMethod = typeof(Console).GetMethod("WriteLine", new[] { typeof(string), typeof(object) });
           il.Call(writeLineMethod); // Console.WriteLine(format, obj.field.ToString()); stack: []
       }
       il.Ret();
   }
   return (Action<T>)method.CreateDelegate(typeof(Action<T>));
}


Пробежимся по всем предыдущим ошибкам и посмотрим, как это будет выглядеть с GroboIL'ом.


  1. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       il.Ldfld(); // ← Здесь будет ошибка компиляции
       {..} // Здесь какие-то инструкции
    }
    

    Будет ошибка компиляции, так как нет перегрузки метода GroboIL.Ldfld() без параметров.


  2. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       il.Box(); // ← Здесь будет ошибка компиляции
       {..} // Здесь какие-то инструкции
    }
    

    Будет ошибка компиляции, так как нет перегрузки метода GroboIL.Box() без параметров.


  3. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       var code = GetCode(..); // Функция возвращает byte
       il.Ldc_I4(code); // ← Здесь все ок, будет принят int
       {..} // Здесь какие-то инструкции
    }
    

    Метод GroboIL.Ldc_I4() принимает int, поэтому byte скастуется к int и все будет правильно.


  4. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       il.Call(abstractMethod); // ← Здесь все ок, будет сгенерирована инструкция Callvirt
       {..} // Здесь какие-то инструкции
    }
    

    Функция GroboIL.Call() эмитит OpCodes.Call для невиртуальных методов и OpCodes.Callvirt для виртуальных (если нужно вызвать виртуальный метод невиртуально, например, вызвать базовую реализацию, то нужно использовать метод GroboIL.Callnonvirt())


  5. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       var keyGetter = typeof(KeyValuePair<int, int>).GetProperty("Key").GetGetMethod();
       il.Ldarg(1); // Аргумент 1 – KeyValuePair<int, int>
       il.Call(keyGetter); // ← Здесь вылетит исключение
       {..} // Здесь какие-то инструкции
    }
    

    Валидатор стэка выдаст ошибку, что нельзя вызвать метод на value type:
    InvalidOperationException: «In order to call the method 'String KeyValuePair<Int32, String>.get_Value()' on a value type 'KeyValuePair<Int32, String>' load an instance by ref or box it».


  6. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       var toStringMethod = typeof(object).GetMethod("ToString");
       il.Ldarga(1); // Аргумент 1 – int, загрузили по адресу
       il.Call(toStringMethod); // ← Здесь вылетит исключение
       {..} // Здесь какие-то инструкции
    }
    

    Валидатор стэка выдаст ошибку, что для вызова виртуального метода на value type нужно передать параметр ‘constrained’ (который подставит префикс OpCodes.Constrained):
    InvalidOperationException: «In order to call a virtual method 'String Object.ToString()' on a value type 'KeyValuePair<Int32, String>' specify the 'constrained' parameter».


  7. using(var il = new GroboIL(dynamicMethod))
    {
       {..} // Здесь какие-то инструкции
       var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; // Хотим достать приватное поле value
       var valueField = typeof(KeyValuePair<int, string>).GetField("value", bindingFlags);
       il.Ldarga(1); // Аргумент 1 – KeyValuePair<string, int>
       il.Ldfld(valueField); // ← Здесь вылетит исключение
       {..} // Здесь какие-то инструкции
    }
    

    Валидатор стэка выдаст ошибку, что не может загрузить поле:
    InvalidOperationException: «Cannot load the field 'KeyValuePair<Int32, String>.value' of an instance of type 'KeyValuePair<String, Int32>'».

  8. Есть проверка, что любая программа заканчивается на одну из нескольких допустимых инструкций, в частности, на OpCodes.Ret.


  9. using(var il = new GroboIL(dynamicMethod))
    {
       il.Ldarg(0); // Аргумент 0 - типа int
       il.Ldarg(1); // Аргумент 1 - типа double
       il.Add(); // ← Здесь вылетит исключение
       il.Ret();
    }
    

    Валидатор стэка выдаст ошибку, что инструкция OpCodes.Add невалидна в текущем контексте:
    InvalidOperationException: «Cannot perform the instruction 'add' on types 'Int32' and 'Double'».


  10. using(var il = new GroboIL(dynamicMethod))
    {
       il.Ldarg(0); // stack: [str]
       il.Dup(); // stack: [str, str]
       var notNullLabel = il.DefineLabel("notNull");
       il.Brtrue(notNullLabel); // if(str != null) goto notNull; stack: [str]
       il.Ldstr(""); // Oops, забыли, что на стэке еще осталось значение str
       il.MarkLabel(notNullLabel); // ← Здесь вылетит исключение
       il.Ret();
    }
    

    Валидатор стэка выдаст ошибку, что два пути исполнения кода формируют разный стэк вычислений, и покажет содержимое стэка в обоих случаях:
    InvalidOperationException: «Inconsistent stack for the label ‘notNull’
    Stack #1: [null, String]
    Stack #2: [String]»


Debugging


Помимо прочего, GroboIL формирует дебаг-текст генерируемого IL-кода, где справа от каждой инструкции написано содержимое стэка, который можно получить, вызвав GroboIL.GetILCode(), например:


     ldarg.0          // [List<T>]
     dup              // [List<T>, List<T>]
     brtrue notNull_0 // [null]
     pop              // []
     ldc.i4.0         // [Int32]
     newarr T         // [T[]]
notNull_0:            // [{Object: IList, IList<T>, IReadOnlyList<T>}]
     ldarg.1          // [{Object: IList, IList<T>, IReadOnlyList<T>}, Func<T, Int32>]
     call Int32 Enumerable.Sum<T>(IEnumerable<T>, Func<T, Int32>)
                      // [Int32]
     ret              // []


Ну и напоследок, имеется возможность дебагать MethodBuillder'ы. В этом случае GroboIL автоматически строит символьную информацию, где исходным текстом является приведенный выше дебаг-текст.

Пример:

public abstract class Bazzze
{
   public abstract int Sum(int x, double y);
}

public void Test()
{
   var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
                      new AssemblyName("DynAssembly"),
                      AssemblyBuilderAccess.RunAndCollect); // Хотим, чтобы сборщик собрал Assembly, когда она станет не нужна
   var module = assembly.DefineDynamicModule("zzz", "zzz.dll", true); // true - хотим строить символьную информацию
   var symWriter = module.GetSymWriter();
   var typeBuilder = module.DefineType("Zzz", TypeAttributes.Public | TypeAttributes.Class, typeof(Bazzze));
   var method = typeBuilder.DefineMethod(
                    "Sum",
                    MethodAttributes.Public | MethodAttributes.Virtual, // Будем перегружать метод базового класса
                    typeof(int), // Возвращаемый тип
                    new[] { typeof(int), typeof(double) }); // Типы аргументов
   method.DefineParameter(1, ParameterAttributes.None, "x"); // Нужно только для дебага
   method.DefineParameter(2, ParameterAttributes.None, "y"); // Эти имена можно вводить в watch
   var documentName = typeBuilder.Name + "." + method.Name + ".cil";
   var documentWriter = symWriter.DefineDocument(documentName,
                            SymDocumentType.Text, SymLanguageType.ILAssembly, Guid.Empty); // Здесь можно любые гуиды ставить
   using(var il = new GroboIL(method, documentWriter)) // Передаем в конструктор documentWriter
   {
       il.Ldarg(1); // stack: [x]
       il.Ldarg(2); // stack: [x, y]
       il.Conv<int>(); // stack: [x, (int)y]
       il.Dup(); // stack: [x, (int)y, (int)y]
       var temp = il.DeclareLocal(typeof(int), "temp");
       il.Stloc(temp); // temp = (int)y; stack: [x, (int)y]
       il.Add(); // stack: [x + (int)y]
       il.Ret();

       File.WriteAllText(Path.Combine(DebugOutputDirectory, documentName), il.GetILCode());
   }
   typeBuilder.DefineMethodOverride(method, typeof(Bazzze).GetMethod("Sum")); // Перегружаем метод

   var type = typeBuilder.CreateType();
   var inst = (Bazzze)Activator.CreateInstance(type, new object[0]);

   inst.Sum(10, 3.14);
}


Теперь ставим брэйкпоинт на строку inst.Sum(10, 3.14); и нажимаем F11 (step into), выпадет диалоговое окно:



В открывшемся окне выбираем папку, куда был сложен дебаг-файлик, и увидим примерно следующее:



Этот файл Visual Studio воспринимает как обычный исходник, можно дебагать по F10/F11, ставить брэйкпоинты, в watch можно вводить параметры функции, this, локальные переменные.

К сожалению, так же красиво дебагать DynamicMethod'ы не получится, поскольку у них отсутствует встроенный механизм построения символьной информации (если кто-то из читателей знает такой способ, я был бы рад услышать). Но, так как IL-команды одинаковые как для DynamicMethod'а, так и для MethodBuilder'а, то можно спроектировать код так, что в нем будет легко подменить DynamicMethod на MethodBuilder для дебага, а в релиз-версии отключить.

Вывод


С высоты своего пятилетнего опыта генерации IL-кода могу сделать следующий вывод: разница в разработке кода на ILGenerator и GroboIL сравнима с разницей в разработке на C# в VisualStudio с решарпером и разработке в блокноте с компилятором, который говорит ответ в виде Accepted/Rejected без номера строки с ошибкой. Разница в скорости разработки – на порядок. На мой взгляд, GroboIL позволяет генерировать IL-код практически с той же скоростью, что и генерировать, например, C#-код, оставляя при этом все преимущества языка низкого уровня.
Проголосовать:
+26
Сохранить: