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

от автора

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

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


Комментарии

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

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