Привет, Хабр! Представляю вашему вниманию перевод статьи "Using Reflection.Emit to Precompile Expressions to MSIL" автора Steve Marsh.

Введение


Классы в этом проекте позволяют анализировать текстовые выражения, введенные пользователем, и компилировать их на сборку .NET. Эта сборка может выполняться «на лету» или сохраняться в DLL. Предварительная компиляция выражений позволяет обеспечить высокий уровень переносимости и позволяет очень эффективно оценивать введенную пользователем логику. Кроме того, мы можем использовать средство ildasm.exe от Microsoft для открытия и проверки создаваемого базового кода MSIL. Есть много интересных функций, которые поставляются с платформой .NET, на мой взгляд пространство имен Reflection.Emit предлагает куда больше, чем вы сможете найти. Пространство имен Reflection.Emit позволяет создавать собственный код .NET во время выполнения, динамически создавая типы .NET и вставляя инструкции MSIL в тело. MSIL — промежуточный язык Microsoft для платформы .NET. IL — это то, во что ваш код C # и VB.NET компилируется и отправляется в JIT-компилятор при запуске .NET-программ. MSIL — очень низкоуровневый язык, который очень быстрый, и работа с ним дает вам исключительный контроль над вашими программами. Я не буду вдаваться в подробности о MSIL в этой статье, но есть несколько других ресурсов, доступных в Интернете, и если вам интересно узнать побольше, я включил некоторые ссылки в конце этой статьи.

Справочная информация


Давайте кратко рассмотрим, что будет делать наш синтаксический анализатор / компилятор. Пользователь вводит строковое выражение, соответствующее грамматике нашего парсера. Это выражение будет превращено в крошечную .NET-программу, которая будет запускать и
выводить результат.Для этого анализатор будет читать в последовательном списке символов и разбивать его на иерархическое дерево, как показано ниже. Узлы оцениваются в указанном порядке. Когда узел сопоставляется, соответствующая команда вызывается для этого типа узла. Например, когда число совпадает, мы отправляем это число в стек. Когда токен «*» согласован, мы вызываем инструкцию умножения и так далее. Добавление всех инструкций в правильном порядке дает нам «программу», показанную справа.

image

Теперь давайте посмотрим, как наша программа выполняет и сравнивает ее с исходным текстовым выражением. Первые две команды вставляют целые числа 3 и 2 в стек. Команда multiply извлекает эти два значения из стека, умножает их и отправляет результат 6 обратно в стек. Инструкция № 4 отправляет целое число 1 в стек. Инструкция № 5 выводит два значения (6 и 1), добавляет их и возвращает результат (7) обратно в стек. Наконец, команда return выдает значение 7 из стека и возвращает его в качестве результата.Блестяще! Это может показаться простым и очевидным для большинства программистов, но эта умная идея в значительной степени является основой для программирования и компиляции, и я думаю, что это стоит посмотреть. Вот как выглядит эта программа в MSIL. Например, ldc.r8 представляет собой команду load constant и загружает double 3.0 в стек.

IL_0000: ldc.r8 3.
IL_0009: ldc.r8 2.
IL_0012: mul
IL_0013: ldc.r8 1.
IL_001c: add
IL_0023: ret

Использование кода


Этот проект содержит два класса для разбора выражения и компиляции его в MSIL. Первый класс — это RuleParser, который является абстрактным классом синтаксического анализа, который содержит всю логику лексинга и разбора для нашей конкретной грамматики. Этот класс анализирует сообщение, но не предпринимает никаких действий. Вышеприведенный фрагмент кода показывает, что при обнаружении токена ttAdd парсер вызывает метод matchAdd (), который является абстрактным методом, определенным в классе RuleParser. Реализация метода класса и соответствующего семантического действия зависит от конкретного класса. Этот шаблон позволяет нам реализовать отдельный конкретный класс для обработки семантических действий и означает, что мы можем реализовать разные конкретные классы в зависимости от того, что мы пытаемся выполнить. Этот код ранее был настроен для оценки выражений «на лету» путем вычисления узлов, как только они были найдены. Теперь мы можем обменять наш MsilParser на компиляцию выражения в программу IL, используя тот же класс парсера.MsilParser делает это, реализуя все необходимые функции токена и испуская соответствующие IL-инструкции. Например, функция matchAdd () просто вставляет команду Add. Когда переменная сопоставляется, мы загружаем имя переменной командой Ldstr, а затем вызываем метод GetVar.

protected override void matchAdd()
{
    this.il.Emit(OpCodes.Add);
}
protected override void matchVar()
{
    string s = tokenValue.ToString();
    il.Emit(OpCodes.Ldstr, s);
    il.Emit(OpCodes.Call, typeof(MsilParser).GetMethod(
            "GetVar", new Type[] { typeof(string) }));
}

После установки всех токенов мы можем вызвать метод CompileMsil () нашего класса MsilParser, который запускает синтаксический анализатор и возвращает скомпилированный .NET-тип, используя классы AssemblyBuilder в пространстве имен Relection.Emit.

/// <summary>
/// Builds and returns a dynamic assembly
/// </summary>
public Type CompileMsil(string expr)
{
    // Build the dynamic assembly
    string assemblyName = "Expression";
    string modName = "expression.dll";
    string typeName = "Expression";
    string methodName = "RunExpression";
    AssemblyName name = new AssemblyName(assemblyName);
    AppDomain domain = System.Threading.Thread.GetDomain();
    AssemblyBuilder builder = domain.DefineDynamicAssembly(
      name, AssemblyBuilderAccess.RunAndSave);
    ModuleBuilder module = builder.DefineDynamicModule
      (modName, true);
    TypeBuilder typeBuilder = module.DefineType(typeName,
      TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodName,
      MethodAttributes.HideBySig | MethodAttributes.Static
      | MethodAttributes.Public,
      typeof(Object), new Type[] {  });
    // Create the ILGenerator to insert code into our method body
    ILGenerator ilGenerator = methodBuilder.GetILGenerator();
    this.il = ilGenerator;
    // Parse the expression. This will insert MSIL instructions
    this.Run(expr);
    // Finish the method by boxing the result as Double
    this.il.Emit(OpCodes.Conv_R8);
    this.il.Emit(OpCodes.Box, typeof(Double));
    this.il.Emit(OpCodes.Ret);
    // Create and save the Assembly and return the type
    Type myClass = typeBuilder.CreateType();
    builder.Save(modName);
    return myClass;
}

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

.method public hidebysig static object
        RunExpression() cil managed
 {
   // Code size       36 (0x24)
   .maxstack  2
   IL_0000:  ldc.r8     3.
   IL_0009:  ldc.r8     2.
   IL_0012:  mul
   IL_0013:  ldc.r8     1.
   IL_001c:  add
   IL_001d:  conv.r8
   IL_001e:  box        [mscorlib]System.Double
   IL_0023:  ret
 } // end of method Expression::RunExpression

Основное преимущество этого подхода состоит в том, что разбор выражения занимает гораздо больше времени, чем просто выполнение инструкций. Предварительно компилируя выражение в IL, нам нужно только разобрать выражение один раз, а не каждый раз, когда он оценивается. Хотя в этом примере используется только одно выражение, реальная реализация может включать тысячи выражений, предварительно скомпилированных и исполняемых по требованию. Кроме того, у нас также есть наш код, упакованный в хорошую .NET DLL, и мы можем делать все, что захотим. Этот пример можно оценить более 1 миллиона раз быстрее чем за 3 сотые доли секунды!

Использование образца проекта


Пример проекта позволяет ввести выражение в верхнем левом текстовом поле. Когда вы нажмете «Анализ», форма проанализирует выражение и создаст сборку .NET с вашим скомпилированным кодом в функции RunExpression (). Затем программа вызовет эту функцию определенное количество раз и покажет, сколько времени потребовалось для ее выполнения. Наконец, программа сохранит сборку как expression.dll и запустит файл ildasm.exe от Microsoft для вывода полного кода MSIL для сборки, чтобы вы могли увидеть код, который был сгенерирован для вашей программы.

Вопросы, представляющие интерес


Как вызван наш динамический метод значительно повлияет на производительность. Например, просто использование метода Invoke () в динамическом методе значительно замедлит производительность при вызове 1 миллион раз. Использование общей подписки делегата, как и в приведенном ниже коде, дает нам примерно 20-кратное повышение производительности.

image

// Parse the expression and build our dynamic method
MsilParser em = new MsilParser();
Type t = em.CompileMsil(textBox1.Text);         
// Get a typed delegate reference to our method. This is very 
// important for efficient calls!
MethodInfo m = t.GetMethod("RunExpression");
Delegate d = Delegate.CreateDelegate(typeof(MsilParser.ExpressionInvoker<>), m);
MsilParser.ExpressionInvoker<> method = 
(MsilParser.ExpressionInvoker<>)d;
// Call the function
Object result = method();

*в пустых угловых скобках должен быть Object.

Вызов ILDASM.EXE


Образец проекта также позволит вам просмотреть весь код MSIL для вашей вновь созданной сборки. Он делает это, вызывая ildasm.exe в фоновом режиме и выводя результат в текстовое поле. Ildasm.exe — очень полезный инструмент для тех, кто работает с IL-кодом или пространством имен System.Reflection.Emit. В приведенном ниже коде показано, как использовать этот исполняемый файл в вашей программе, используя пространство имен System.Diagnostics. Ознакомьтесь с документацией Microsoft по ildasm.exe по ссылкам ниже.

// Save the Assembly and generate the MSIL code with ILDASM.EXE
string modName = "expression.dll";
Process p = new Process();
p.StartInfo.FileName = "ildasm.exe";
p.StartInfo.Arguments = "/text /nobar \"" + modName;
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.Start();
string s = p.StandardOutput.ReadToEnd();
p.WaitForExit();
p.Close();
txtMsil.Text = s;

Ссылки:


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


  1. AnutaU
    19.03.2018 16:16

    Узлы оцениваются

    Вычисляются
    Теперь давайте посмотрим, как наша программа выполняет и сравнивает ее с исходным текстовым выражением.

    Посмотрим, как программа выполняется, и сравним
    общей подписки делегата

    Это гуглтранслейт так перевёл «generic delegate signature»?

    Ну и в целом перевод очень низкого качества, сильно искажающий смысл. Увы.