Я много раз сталкивался с задачей динамической генерации кода (например, при написании эффективного сериализатора или компилятора 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#-код, оставляя при этом все преимущества языка низкого уровня.

Комментарии (15)


  1. atd
    15.07.2015 18:31
    +6

    А я пересел на Expression.Lambda<>().Compile() и забыл про IL как страшный сон )

    > Разница в скорости разработки – на порядок

    Попробуйте и вы, скорость разработки возрастёт на ещё один порядок


    1. kekekeks
      15.07.2015 20:21
      +2

      Expression.Compile не дружит со штуками типа Mono.Cecil и dnlib. Штуки типа FluentIL умеют с ними интегрироваться.


    1. homuroll Автор
      16.07.2015 09:24

      Вопрос, как именно динамически генерировать код — отдельный разговор. Конечно, можно почти всегда и Expression-ы сделать, или, например, C#. Но не факт, что это будет лучше и быстрее, чем на IL. С Expression'ами у меня тоже большой опыт работы, но у них есть свои проблемы, например, как-то раз я попытался скомпилировать Expression, DebugView которого содержал примерно 900 000 строк. Так стандартный компилятор выделил 24ГБ оперативки, и еще и результат работал очень медленно. В итоге мне пришлось написать свой компилятор, кстати, с использованием GroboIL.


      1. DrReiz
        16.07.2015 10:04
        +1

        Под какую необходимость требуется делать большие монолитные методы на 10-ки тысяч инструкции и более? Почему не получается разбить на генерацию отдельных методов?


        1. homuroll Автор
          16.07.2015 11:24

          От сгенерированного кода требуются две вещи: чтобы он работал и, если нужно, чтобы он работал быстро. Просто было проще и элегантнее сгенерировать один большой метод, в другом месте, например, пришлось все же разбивать, но там другая история — уперлись в производительность JIT-компилятора.


  1. kekekeks
    15.07.2015 20:19
    +3

    1. DreamWalker
      16.07.2015 00:13

      Отличная штука, спасибо!


    1. yallie
      16.07.2015 15:39

      Здорово, что эта библиотека умеет через Mono.Cecil работать.
      Очень существенное преимущество перед аналогами.


  1. PsyHaSTe
    16.07.2015 12:04

    Я наверное очень ленивый или просто глупый, эмиттить руками у меня не хватает способностей.
    Поэтому поступаю просто по-студенчески, в лоб — пишу нужный код руками, декомпилирую, смотрю на полученный IL, и дорабатываю его напильником до нужного состояния. Ошибок в сгенерированном коде гарантированно нет, остается не привнести своих, но это намного проще. Это не откровение, многие думаю так делают, но имхо довольно приятный способ. Хотя вот по ссылке товарища kekekeks всё еще приятнее, может и этот способ отжил свое.


  1. smilegs
    16.07.2015 13:11

    Расскажите, пожалуйста, зачем нужна кодогенерация с использованием IL?
    Если для оптимизации, почему не удобнее наиоблее критичные вещи реализовать с помощью c/c++ и делать unmanaged вызовы? Ведь это должно работать быстрее.
    Какие задачи решаются кодогенерацией?
    Другими словами — почему мне нужно тратить время на это? :)


    1. nsinreal
      16.07.2015 13:19

      Кодогенерация нужна когда нужно кодогенерировать и лениво писать руками. Каноничный пример — материализаторы сущностей из бд; мапперы, сериализаторы.
      В C# есть аж три варианта получения универсального кода: Reflection, Expressions, IL. Рефлексия самая медленная, генерация IL самая быстрая.
      Unmanaged не помогает, потому что есть затраты на то, чтобы делать unmanaged-вызовы. Т.е. сгенерированный IL будет быстрее в случае когда у вас есть очень много коротких по длительности методов.


    1. PsyHaSTe
      16.07.2015 14:00

      Ну например я писал простенькое расширение для преобразования любой структуры в массив байт и обратно со скоростью сравнимой с плюсовой. Для структуры одного типа сделать это легко, а для дженерика — язык запрещает (но не среда). А раз так, то легко её обмануть, написав на чистом IL:

      github.com/Pzixel/StructInterop/blob/master/StructInterop/StructInterop/StructInterop.cs


  1. yallie
    16.07.2015 15:24

    Все-таки гораздо удобнее использовать высокоуровневые обертки вместо генерации IL.
    Чтобы сразу запустить код хватает Expression.Compile(), а чтобы сохранить на диск — что-то типа RunSharp:

    AssemblyGen ag = new AssemblyGen("hello.exe");
    TypeGen Test = ag.Public.Class("Test");
    {
       CodeGen g = Test.Public.Static.Method(typeof(void), "Main", typeof(string[]));
       {
          Operand args = g.Param(0, "args");
          g.Invoke(typeof(Console), "WriteLine", "Hello " + args[0] + "!");
       }
    }
    ag.Save();
    

    Более сложный пример с генерацией метода с локальными переменными, с циклом и пр.
    Взято отсюда: github.com/yallie/RunSharp/blob/master/Examples/08_Indexers.cs

    // Demonstrate the FileByteArray class.
    // Reverses the bytes in a file.
    TypeGen Reverse = ag.Public.Class("Reverse");
    {
    	CodeGen g = Reverse.Public.Static.Void("Main").Parameter<string[]>("args");
    	{
    		Operand args = g.Arg("args");
    
    		// Check for arguments.
    		g.If(args.ArrayLength() != 1);
    		{
    			g.WriteLine("Usage : Indexer <filename>");
    			g.Return();
    		}
    		g.End();
    
    		// Check for file existence
    		g.If(!Static.Invoke(typeof(File), "Exists", args[0]));
    		{
    			g.WriteLine("File " + args[0] + " not found.");
    			g.Return();
    		}
    		g.End();
    
    		Operand file = g.Local(Exp.New(FileByteArray, args[0]));
    		Operand len = g.Local(file.Property("Length"));
    
    		// Swap bytes in the file to reverse it.
    		Operand i = g.Local<long>();
    		g.For(i.Assign(0), i < len / 2, i.Increment());
    		{
    			Operand t = g.Local();
    
    			// Note that indexing the "file" variable invokes the
    			// indexer on the FileByteStream class, which reads
    			// and writes the bytes in the file.
    			g.Assign(t, file[i]);
    			g.Assign(file[i], file[len - i - 1]);
    			g.Assign(file[len - i - 1], t);
    		}
    		g.End();
    
    		g.Invoke(file, "Close");
    	}
    }
    


    1. PsyHaSTe
      16.07.2015 18:23

      Чтобы сохранить на диск можно использовать извращения вроде CSharpCodeProvider


      1. yallie
        16.07.2015 19:08

        CSharpCodeProvider вызывает внешний компилятор csc.exe, чтобы скомпилировать файл.
        А у нас тут вроде обсуждается генерация кода на лету без использования компилятора.