在.NET框架中,动态编译技术允许开发者在运行时创建和执行代码。这种技术的核心是MSIL(Microsoft Intermediate Language),它是.NET程序编译后的中间语言,由JIT编译器在运行时转换为机器代码。本文将介绍如何利用.NET的Reflection.Emit命名空间动态生成代码,并使用ildasm.exe工具查看生成的MSIL代码。
MSIL是.NET框架的中间语言,所有.NET语言(如C#和VB.NET)编写的代码最终都会被编译成MSIL。MSIL是一种低级别的语言,执行效率高,允许开发者对程序有更精细的控制。虽然本文不会深入探讨MSIL的细节,但有兴趣的读者可以通过文末提供的链接了解更多信息。
在动态编译的场景中,用户输入一个符合特定语法的表达式,该表达式会被转换成一个小型的.NET程序并执行,输出结果。这个过程涉及到解析器(parser)和编译器(compiler),解析器将输入的字符序列转换成一个层次化的解析树,然后按照一定的顺序评估这些节点。
以表达式“3 * 2 + 1”为例,解析器首先将数字3和2压入栈中,然后执行乘法操作,将结果6压回栈中。接着,将数字1压入栈中,执行加法操作,最终将结果7压回栈中。最后,return命令将栈顶的值7返回作为结果。
.NET框架提供了Reflection.Emit命名空间,它允许在运行时动态创建.NET类型,并将MSIL指令插入到这些类型的方法体中。这种动态代码生成的能力为开发者提供了极大的灵活性。
在本项目中,定义了两个类:RuleParser和MsilParser。RuleParser是一个抽象的解析类,包含了特定语法的解析逻辑。MsilParser则实现了将表达式编译成MSIL代码的功能。
RuleParser类负责解析输入的表达式,但不执行任何操作。当解析到特定的token时,它会调用对应的抽象方法,如matchAdd()。具体的语义动作由具体的子类实现。
MsilParser类通过实现所有必要的token函数并发出相应的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) }));
}
在所有token设置完成后,可以调用MsilParser类的CompileMsil()方法,该方法运行解析器并使用Reflection.Emit命名空间的AssemblyBuilder类返回编译后的.NET类型。
public Type CompileMsil(string expr)
{
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[] { });
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
this.il = ilGenerator;
this.Run(expr);
this.il.Emit(OpCodes.Conv_R8);
this.il.Emit(OpCodes.Box, typeof(Double));
this.il.Emit(OpCodes.Ret);
Type myClass = typeBuilder.CreateType();
builder.Save(modName);
return myClass;
}
通过预编译表达式到IL,只需要解析一次表达式,而不是每次评估时都解析。虽然这个例子只使用了单个表达式,但在实际应用中,可以预编译数千个表达式,并按需执行。此外,代码被打包成一个.NETDLL,可以随意使用。这个示例可以在不到0.03秒的时间内执行超过100万次!
示例项目允许在左上角的文本框中输入一个表达式。当点击“解析”时,表单将解析表达式并创建一个包含编译代码的.NET程序集。然后,程序将调用该函数指定的次数,并显示执行所需的时间。最后,程序将程序集保存为expression.dll,并运行Microsoft的ildasm.exe来输出程序集的完整MSIL代码,以便可以看到为程序生成的代码。
MsilParser em = new MsilParser();
Type t = em.CompileMsil(textBox1.Text);
MethodInfo m = t.GetMethod("RunExpression");
Delegate d = Delegate.CreateDelegate(typeof(MsilParser.ExpressionInvoker
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;