Я много раз сталкивался с задачей динамической генерации кода (например, при написании эффективного сериализатора или компилятора DSL). Это можно делать разными способами, какой из них лучший – дискуссия для отдельной статьи. По ряду причин я предпочитаю Reflection.Emit и CIL (Common Intermediate Language) и расскажу, с какими проблемами пришлось столкнуться на этом пути, а также об их решении: умной обертке над ILGenerator – GroboIL из библиотеки 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».
Примеры ошибок/опечаток
-
var il = dynamicMethod.GetILGenerator(); {..} // Здесь какие-то инструкции il.Emit(OpCodes.Ldfld); // Пытаемся загрузить поле, но забыли передать FieldInfo {..} // Здесь какие-то инструкции var compiledMethod = dynamicMethod.CreateDelegate(..); compiledMethod(..); // ← Здесь вылетит исключение
InvalidProgramException: «Common language runtime detected an invalid program».
-
var il = dynamicMethod.GetILGenerator(); {..} // Здесь какие-то инструкции il.Emit(OpCodes.Box); // Хотели скастовать value type к object, но забыли передать тип {..} // Здесь какие-то инструкции var compiledMethod = dynamicMethod.CreateDelegate(..); compiledMethod(..); // ← Здесь вылетит исключение
InvalidProgramException: «Common language runtime detected an invalid program».
-
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».
-
var il = dynamicMethod.GetILGenerator(); {..} // Здесь какие-то инструкции il.Emit(OpCodes.Call, abstractMethod); // Хотели вызвать абстрактный метод, но случайно вместо Callvirt написали Call {..} // Здесь какие-то инструкции var compiledMethod = dynamicMethod.CreateDelegate(..); compiledMethod(..); // ← Здесь вылетит исключение
BadImageFormatException: «Invalid il format».
-
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».
-
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». -
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.
- Забыли в конце кода вызвать инструкцию OpCodes.Ret – получим неопределенное поведение: может, вылетит исключение при попытке компиляции, может просто все сломаться уже во время работы, а может повезти и все будет работать правильно.
- Реализуем функцию
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).
- x64: compiledMethod(10, 3.14) = 13
- Реализуем функцию
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‘ом.
-
using(var il = new GroboIL(dynamicMethod)) { {..} // Здесь какие-то инструкции il.Ldfld(); // ← Здесь будет ошибка компиляции {..} // Здесь какие-то инструкции }
Будет ошибка компиляции, так как нет перегрузки метода GroboIL.Ldfld() без параметров.
-
using(var il = new GroboIL(dynamicMethod)) { {..} // Здесь какие-то инструкции il.Box(); // ← Здесь будет ошибка компиляции {..} // Здесь какие-то инструкции }
Будет ошибка компиляции, так как нет перегрузки метода GroboIL.Box() без параметров.
-
using(var il = new GroboIL(dynamicMethod)) { {..} // Здесь какие-то инструкции var code = GetCode(..); // Функция возвращает byte il.Ldc_I4(code); // ← Здесь все ок, будет принят int {..} // Здесь какие-то инструкции }
Метод GroboIL.Ldc_I4() принимает int, поэтому byte скастуется к int и все будет правильно.
-
using(var il = new GroboIL(dynamicMethod)) { {..} // Здесь какие-то инструкции il.Call(abstractMethod); // ← Здесь все ок, будет сгенерирована инструкция Callvirt {..} // Здесь какие-то инструкции }
Функция GroboIL.Call() эмитит OpCodes.Call для невиртуальных методов и OpCodes.Callvirt для виртуальных (если нужно вызвать виртуальный метод невиртуально, например, вызвать базовую реализацию, то нужно использовать метод GroboIL.Callnonvirt())
-
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». -
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». -
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>’». - Есть проверка, что любая программа заканчивается на одну из нескольких допустимых инструкций, в частности, на OpCodes.Ret.
-
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’». -
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/
Добавить комментарий