之前的Hello World例子应该已经让我们对Emit有了一个模糊的了解,那么Emit到底是什么样一个东西,他又能实现些什么功能呢?昨天查了点资料,大致总结了下,由于才开始学习肯定有不完善的地方,希望大家能够批评指正。
1. 什么是反射发出(Reflection Emit)
Emit应该是属于反射中的一个比较高级的功能,说到反射大家应该都不陌生,反射是在运行时发现对象的相关信息,并且执行这些对象(创建对象实例,执行对象上的方法)。这个功能是由.NET的System.Reflection命名空间的类所提供的。简单的说,它们不仅允许你浏览一个程序集暴露的类、方法、属性和字段,而且还允许你创建一个类型的实例以及执行这些类型上的方法(调用成员)。这些特性对于在运行时对象发现,已经很了不起了,但.NET的反射机制并没有到此结束。反射还允许你在运行时构建一个程序集,并且可以创建全新的类型。这就是反射发出(reflection emit)。
使用Emit可以从零开始,动态的构造程序集和类型,在需要时动态的生成代码,提高程序的灵活性。有了这些功能,我们可以用其来实现一些典型的应用,如:
- 动态代理(AOP);
- 减少反射的性能损失(Dynamic Method等);
- ORM的实现;
- 工具及IDE插件的开发;
- 公共代码安全模块的开发。
2. 使用Emit的完整流程
使用Emit一般包括以下步骤:
1) 创建一个新的程序集(可以选择存在与内存中或者持久化到硬盘);
2) 在程序集内创建一个模块;
3) 在模块内创建动态类;
4) 给动态类添加动态方法、属性、事件,等;
5) 生成相关的IL代码;
6) 返回创建出来的类型或持久化到硬盘中。
当然如果你只是想要创建一个Dynamic Method 那么可以直接使用之前HelloWorld例子中使用的DynamicMethod类来创建一个动态方法,并在构造函数时传入它所依附的类或者模块。看了这个流程,相信大家已经对用使用Emit来创建动态类型的过程有了一个直观的认识,下面我们就通过实现一个求斐波那契数列的类来加深对这一流程的了解。
在开始我们的例子之前,先给大家介绍一款反编译软件Reflector,使用这个软件可以给我们编写IL代码提供很大的帮助。
接下来我们按照上面所说的流程来创建我们的斐波那契类:
第一步:构建程序集
要构建一个动态的程序集,我们需要创建一个AssemblyBuilder对象,AssemblyBuilder类是整个反射发出工作的基础,它为我们提供了动态构造程序集的入口。要创建一个AssemblyBuilder对象,需要使用AppDomain的DefineDynamicAssembly方法,该方法包括两个最基本的参数:AssemblyName和AssemblyBuilderAccess前者用来唯一标识一个程序集,后者用来表示动态程序集的访问方式,有如下的成员:
成员名称
说明
Run
表示可以执行但不能保存此动态程序集。
Save
表示可以保存但不能执行此动态程序集。
RunAndSave
表示可以执行并保存此动态程序集。
ReflectionOnly
表示在只反射上下文中加载动态程序集,且不能执行此程序集。
在这里我们选择使用RunAndSave,完整的代码如下:
* Step 1 构建程序集
//创建程序集名 AssemblyName asmName = new AssemblyName("EmitExamples.DynamicFibonacci"); //获取程序集所在的应用程序域 //你也可以选择用AppDomain.CreateDomain方法创建一个新的应用程序域 //这里选择当前的应用程序域 AppDomain domain = AppDomain.CurrentDomain; //实例化一个AssemblyBuilder对象来实现动态程序集的构建 AssemblyBuilder assemblyBuilder = domain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
第二步:定义模块(Module)
与第一步类似,要定一个动态模块,我们需要创建一个ModuleBuilder对象,通过AssemblyBuilder对象的DefineDynamicModule方法,需要传入模块的名字(如果要持久化到硬盘,那么还需要传入要保存的文件的名字,这里就是我们的程序集名),这里我们使用程序集名作为模块名字:
* Step 2 定义模块
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name, asmFileName);
第三部:创建一个动态类型
这个时候恐怕我不说你也已经知道了,对,现在我们就是要用ModuleBuilder来创建一个TypeBuilder的对象,如下:
* Step 3 定义类型
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitExamples.DynamicFibonacci", TypeAttributes.Public);
这里EmitExamples表示名字空间,DynamicFibonacci是类的名字,TypeAttributes表示类的属性,可以按照实际需要进行组合。
第四步:定义方法
到这里为止,我们的准备工作已经差不多了,下面要开始真正的大展拳脚啦!
我们先来看一下我们接下来要实现的动态类C#代码的实现,然后再以这为目标进行动态构建:
public class Fibonacci { public int Calc(int num) { if (num == 1 || num == 2) { return 1; } else { return Calc(num - 1) + Calc(num - 2); } } }
OK,从上面的代码可以看出我们需要创建一个名为Calc的Public方法,它具有一个Int32型的传入参数和返回值。同样的,我们使用TypeBuilder的DefineMethod方法来创建这样一个MethodBuilder,如下:
* Step 4 定义方法
MethodBuilder methodBuilder = typeBuilder.DefineMethod( "Calc", MethodAttributes.Public, typeof(Int32), new Type[] { typeof(Int32) });
DefineMethod方法的四个参数分别是函数名,修饰符,返回值类型,传入参数的类型数组。
第五步:实现方法
现在就要为之前创建的Calc方法添加对应的IL代码了,这对我们这些新手来说这就显的有点无从入手来了,不过没关系,还记得我之前提到的那个反编译工具吗?现在就是它发挥作用的时候了,我们用它来反编译之前写的Fibonacci类,看看自动生成的IL代码是什么样的,结果如下:
.method public hidebysig instance int32 Calc(int32 num) cil managed { .maxstack 4 .locals init ( [0] int32 CS$1$0000, [1] bool CS$4$0001) L_0000: nop L_0001: ldarg.1 L_0002: ldc.i4.1 L_0003: beq.s L_000e L_0005: ldarg.1 L_0006: ldc.i4.2 L_0007: ceq L_0009: ldc.i4.0 L_000a: ceq L_000c: br.s L_000f L_000e: ldc.i4.0 L_000f: stloc.1 L_0010: ldloc.1 L_0011: brtrue.s L_0018 L_0013: nop L_0014: ldc.i4.1 L_0015: stloc.0 L_0016: br.s L_002f L_0018: nop L_0019: ldarg.0 L_001a: ldarg.1 L_001b: ldc.i4.1 L_001c: sub L_001d: call instance int32 EmitExamples.Fibonacci::Calc(int32) L_0022: ldarg.0 L_0023: ldarg.1 L_0024: ldc.i4.2 L_0025: sub L_0026: call instance int32 EmitExamples.Fibonacci::Calc(int32) L_002b: add L_002c: stloc.0 L_002d: br.s L_002f L_002f: ldloc.0 L_0030: ret }
我们来对上面的IL代码进行分析:
- 从L_0000到L_0003是加载参数一、加载整数1,然后判断两者是否相等,如果相等则跳转到L_000e继续执行;
- 从L_0005到L_000e是加载参数一、加载整数2,然后判断两者是否相等,如果相等则将整数1送到堆栈上,否则将整数0送到堆栈上;然后再加载整数0,用之前比较的结果和0进行比较,如果相等则将整数1送到堆栈上,否则将整数0送到堆栈上;这个时侯,如果传入的参数是2那么现在堆栈上的数字就是两个0,两者相等,那么跳转到L_000f继续执行,反之就继续执行,加载数字0到堆栈上(是不是感觉很复杂,没关系,我们一会对其进行优化);
- 从L_000f到L_0016是判断之前判断的返回值,也就是说如果传入的参数是1或者2,那么就将局部变量0的值设为1,然后跳转到L_002f执行;反之就从L_0018开始执行;
- 从L_0018到L_002b是把参数0和参数1加载(注意:在非静态方法中,参数0表示其对自身所在类的示例的引用,相当于this),然后将参数1分别减去1和2后进行递归调用,并将结果相加,并把记过放到局部变量0中;
- 从L_002d到L_0030是加载局部变量0,并将结果返回。
有了之前分析的基础,我们可以将流程简化为如下步骤:
1) 如果传入的参数是1,跳转到第六步执行;
2) 如果传入的参数是2,跳转到第六步执行;
3) 将传入的参数减1,然后递归调用自身;
4) 将传入的参数减2,然后递归调用自身;
5) 将递归调用的结果相加,跳转到第七步执行;
6) 设置堆栈顶的值为1;
7) 返回堆栈顶的元素作为结果。
然后我们就可以参照以上的反编译出来的IL代码,用Emit书写出对应的IL代码,具体代码如下:
* Step 5 实现方法
ILGenerator calcIL = methodBuilder.GetILGenerator(); //定义标签lbReturn1,用来设置返回值为1 Label lbReturn1 = calcIL.DefineLabel(); //定义标签lbReturnResutl,用来返回最终结果 Label lbReturnResutl = calcIL.DefineLabel(); //加载参数1,和整数1,相比较,如果相等则设置返回值为1 calcIL.Emit(OpCodes.Ldarg_1); calcIL.Emit(OpCodes.Ldc_I4_1); calcIL.Emit(OpCodes.Beq_S, lbReturn1); //加载参数1,和整数2,相比较,如果相等则设置返回值为1 calcIL.Emit(OpCodes.Ldarg_1); calcIL.Emit(OpCodes.Ldc_I4_2); calcIL.Emit(OpCodes.Beq_S, lbReturn1); //加载参数0和1,将参数1减去1,递归调用自身 calcIL.Emit(OpCodes.Ldarg_0); calcIL.Emit(OpCodes.Ldarg_1); calcIL.Emit(OpCodes.Ldc_I4_1); calcIL.Emit(OpCodes.Sub); calcIL.Emit(OpCodes.Call, methodBuilder); //加载参数0和1,将参数1减去2,递归调用自身 calcIL.Emit(OpCodes.Ldarg_0); calcIL.Emit(OpCodes.Ldarg_1); calcIL.Emit(OpCodes.Ldc_I4_2); calcIL.Emit(OpCodes.Sub); calcIL.Emit(OpCodes.Call, methodBuilder); //将递归调用的结果相加,并返回 calcIL.Emit(OpCodes.Add); calcIL.Emit(OpCodes.Br, lbReturnResutl); //在这里创建标签lbReturn1 calcIL.MarkLabel(lbReturn1); calcIL.Emit(OpCodes.Ldc_I4_1); //在这里创建标签lbReturnResutl calcIL.MarkLabel(lbReturnResutl); calcIL.Emit(OpCodes.Ret);
第六步:创建类型,并持久化到硬盘
到上一步为止,我们已经完成了斐波那契类以及方法的完整创建,接下来就是收获的时候了,我们使用TypeBuilder的CreateType方法完成最终的创建过程;最后使用AssemblyBuilder类的Save方法将程序集持久化到硬盘中,代码如下:
* Step 6 收获
Type type = typeBuilder.CreateType(); assemblyBuilder.Save(asmFileName); object ob = Activator.CreateInstance(type); for (int i = 1; i < 10; i++) { Console.WriteLine(type.GetMethod("Calc").Invoke(ob, new object[] { i })); }
这里使用Activator.CreateInstance方法创建了动态类型的一个实例,然后使用MethodInfo的Invoke方法调用里里面的Calc方法,看起来需要通过多次反射,好像性能并不是很好,但其实我们完全可以用Emit来替代掉这两个方法,将反射带来的性能影响降到最低,这个将在以后讲到。