65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Reflection.Emit 将表达式预编译为 MSIL

starIconstarIconstarIconstarIconstarIcon

5.00/5 (30投票s)

2009年1月6日

CPOL

6分钟阅读

viewsIcon

72035

downloadIcon

830

本项目中的类允许您解析用户输入的文本表达式并将其编译为 .NET 程序集。此程序集可以即时执行,或保存到 DLL。

引言

本项目中的类允许您解析用户输入的文本表达式并将其编译为 .NET 程序集。此程序集可以即时执行,或保存到 DLL。预编译表达式具有高度的可移植性,并使我们能够极其高效地评估用户输入的逻辑。此外,我们还可以使用 Microsoft 的 ildasm.exe 工具来打开和检查正在生成的底层 MSIL 代码。

虽然 .NET 框架提供了许多很酷的功能,但对我来说,Reflection.Emit 命名空间提供的“技术乐趣”是最多的。Reflection.Emit 命名空间允许您通过动态创建 .NET 类型并将 MSIL 指令插入到主体中来在运行时创建自己的 .NET 代码。

MSIL 是 Microsoft 面向 .NET 框架的中间语言。IL 是您的 C# 和 VB.NET 代码被编译成的内容,并在运行 .NET 程序时发送给 JIT 编译器。MSIL 是一种非常低级且速度非常快的语言,使用它能够让您对程序进行出色的控制。本文将不会深入探讨 MSIL,但网上有许多其他资源可供参考,如果您有兴趣了解更多信息,我已在本篇文章末尾包含了一些链接。

背景

让我们快速概述一下我们的解析器/编译器将要做什么。用户将输入一个符合我们解析器语法的字符串表达式。此表达式将被转换为一个微小的 .NET 程序,该程序将运行并输出结果。

为了做到这一点,解析器将读取连续的字符列表,并将其分解为如下所示的层次化解析树。节点按所示顺序进行评估。当匹配到一个节点时,将为该节点类型调用相应的指令。例如,当匹配到一个数字时,我们将该数字推送到堆栈上。当匹配到“*”标记时,我们调用乘法指令,依此类推。按正确的顺序累加所有指令,就得到了右侧的“程序”。

表达式:3 * 2 + 1

screen4.gif

现在,让我们看看我们的程序如何执行,并与原始文本表达式进行比较。前两条指令将整数 3 和 2 推送到堆栈上。乘法指令从堆栈弹出这两个值,将它们相乘,并将乘积 6 推回堆栈。指令 #4 将整数 1 推送到堆栈上。指令 #5 弹出这两个值(6 和 1),将它们相加,并将结果(7)推回堆栈。最后,return 命令弹出堆栈上的值 7 并将其作为结果返回。

太棒了!这对于大多数计算机程序员来说可能显得简单明了,但这巧妙的想法几乎是所有编程和编译的基础,我认为值得一看。在 MSIL 中,该程序将如下所示。例如,ldc.r8 代表加载常量指令,并将双精度浮点数 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 类上定义的一个抽象方法。具体类负责实现方法体和相应的语义操作。

screen3.gif

这种模式允许我们实现一个单独的具体类来处理语义操作,这意味着我们可以实现不同的具体类,具体取决于我们想要完成的目标。以前,此代码已设置为通过在找到节点后立即计算节点来即时评估表达式。现在,我们可以切换到我们的 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) }));
}

一旦所有令牌都设置完毕,我们就可以调用我们 MsilParser 类的 CompileMsil() 方法,该方法运行解析器并使用 Relection.Emit 命名空间中的 AssemblyBuilder 类返回已编译的 .NET 类型。

/// <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 中,我们可以对其进行任何操作。此示例可以在不到百分之三秒的时间内评估超过一百万次!

使用示例项目

示例项目允许您在左上角的文本框中输入表达式。当您单击“解析”时,窗体会解析表达式并在 RunExpression() 函数中创建包含已编译代码的 .NET 程序集。然后,程序将指定的次数调用该函数,并显示执行所需的时间。最后,程序会将程序集保存为 expression.dll,并运行 Microsoft 的 ildasm.exe 来输出程序集的完整 MSIL 代码,以便您可以看到为您的程序生成的代码。

screen5.gif

关注点

动态方法如何被调用将对性能产生重大影响。例如,简单地使用动态方法的 Invoke() 方法会在调用一百万次时极大地降低性能。使用通用委托签名(如下面的代码所示)可提供大约 20 倍的性能提升。

// 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<Object>), m);
MsilParser.ExpressionInvoker<Object> method = 
(MsilParser.ExpressionInvoker<Object>)d;
// Call the function
Object result = method();

调用 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;

链接

© . All rights reserved.