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





5.00/5 (30投票s)
本项目中的类允许您解析用户输入的文本表达式并将其编译为 .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 推送到堆栈上。乘法指令从堆栈弹出这两个值,将它们相乘,并将乘积 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
类上定义的一个抽象方法。具体类负责实现方法体和相应的语义操作。
这种模式允许我们实现一个单独的具体类来处理语义操作,这意味着我们可以实现不同的具体类,具体取决于我们想要完成的目标。以前,此代码已设置为通过在找到节点后立即计算节点来即时评估表达式。现在,我们可以切换到我们的 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 代码,以便您可以看到为您的程序生成的代码。
关注点
动态方法如何被调用将对性能产生重大影响。例如,简单地使用动态方法的 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;