GenerateRefEmitAssembly
DiagnosticsSwitches.KeepTempFiles.Enabled = true;
一、System.Reflection.Emit概述
Emit,可以称为发出或者产生。与Emit相关的类基本都存在于System.Reflection.Emit命名空间下。反射,我们可以取得形如程序集包含哪些类型,类型包含哪些方法等等大量的信息,而Emit则可以在运行时动态生成代码。
二、IL代码解析
以下代码为例:
1 static void Main(string[] args) 2 { 3 int i = 1; 4 int j = 2; 5 int k = 3; 6 Console.WriteLine(i+j+k); 7 }
翻译文IL代码为:
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint //程序入口 4 // Code size 19 (0x13) 5 .maxstack 3 //定义函数代码所用堆栈的最大深度,也可理解为Call Stack的变量个数 6 7 //以下我们把它看做是完成代码中的初始化 8 .locals init (int32 V_0,int32 V_1,int32 V_2) //定义 int 类型参数 V_0,V_1,V_2 (此时已经把V_0,V_1,V_2存入了Call Stack中) 9 IL_0000: nop //即No Operation 没有任何操作,我们也不用管它 10 11 IL_0001: ldc.i4.1 //加载第一个变量"i" (压入Evaluation Stack中) 12 IL_0002: stloc.0 //把"i"赋值给Call Stack中第0个位置(V_0) 13 IL_0003: ldc.i4.2 //加载第二个变量"j" (压入Evaluation Stack中) 14 IL_0004: stloc.1 //把"j"赋值给Call Stack中第1个位置(V_1) 15 IL_0005: ldc.i4.3 //加载第三个变量"k" (压入Evaluation Stack中) 16 IL_0006: stloc.2 //把 "k" 赋值给Call Stack中第2个位置(V_2) 17 18 //上面代码初始化完成后要开始输出了,所要把数据从Call Stack中取出 19 20 IL_0007: ldloc.0 //取Call Stack中位置为0的元素(V_0)的值("i"的值) (相当于Copy一份值Call Stack中V_0的值。V_0本身的值是不变的) 21 IL_0008: ldloc.1 //取Call Stack中位置为1的元素(V_1)的值("j"的值) (同上) 22 IL_0009: add // 做加法操作 23 IL_000a: ldloc.2 // 取出Call Stack中位置为2的元素(V_2)的值("k"的值) 24 IL_000b: add // 做加法操作 25 IL_000c: call void [mscorlib]System.Console::WriteLine(int32) //调用输出方法 26 IL_0011: nop 27 IL_0012: ret //即为 return 标记 返回值 28 } // end of method Program::Main
指令详解
Managed Heap::这是动态配置(Dynamic Allocation)的记忆体,由 Garbage Collector(GC)在执行时自动管理,整个 Process 共用一个Managed Heap(我理解为托管堆,存储引用类型的值)。
Evaluation Stack:这是由 .NET CLR 在执行时自动管理的记忆体,每个 Thread 都有自己专属的 Evaluation Stack(我理解为类似一个临时存放值类型数据的线程栈)
Call Stack:这个是由 .NET CLR 在执行时自动管理的记忆体,每个 Thread 都有自己专属的 Call Stack。每呼叫一次 method,就会使得 Call Stack 上多了一個个 Record Frame;呼叫完成之後,此 Record Frame 会被丢弃(我理解为一个局部变量表,用于存放.locals init(int32 V_0)指令的参数值如:V_0)
.maxstack:代码中变量需要在Call Stack 中占用几个位置
.locals init (int32 V_0,int32 V_1,int32 V_2):定义变量并存入Call Stack中
nop:即No Operation 没有任何操作,我们也不用管它,
ldstr.:即Load String 把字符串加压入Evaluation Stack中
stloc.:把Evaluation Stack中的值弹出赋值到Call Stack中
ldloc.:把Call Stack中指定位置的值取出(copy)存入 Evaluation Stack中 以上两条指令为相互的操作stloc赋值,ldloc取值
call: 调用指定的方法
ret: 即return 标记返回
二、动态生成代码
首先我们需要了解每个动态类型在.net中都是用什么类型来表示的。
程序集:System.Reflection.Emit.AssemblyBuilder(定义并表示动态程序集)
构造函数:System.Reflection.Emit.ConstructorBuilder(定义并表示动态类的构造函数)
自定义属性:System.Reflection.Emit.CustomAttributeBuilder(帮助生成自定义属性 使用构造函数传递的参数来生成类的属性)
枚举:System.Reflection.Emit.EnumBuilder(说明并表示枚举类型)
事件:System.Reflection.Emit.EventBuilder(定义类的事件)
字段:System.Reflection.Emit.FieldBuilder(定义并表示字段。无法继承此类)
局部变量:System.Reflection.Emit.LocalBuilder(表示方法或构造函数内的局部变量)
方法:System.Reflection.Emit.MethodBuilder(定义并表示动态类的方法(或构造函数))
模块:System.Reflection.Emit.ModuleBuilder(定义和表示动态程序集中的模块)
参数:System.Reflection.Emit.ParameterBuilder(创建或关联参数信息 如:方法参数,事件参数等)
属性:System.Reflection.Emit.PropertyBuilder(定义类型的属性 (Property))
类:System.Reflection.Emit.TypeBuilder(在运行时定义并创建类的新实例)
以下介绍Emit生成代码的基本流程:
1.构建程序集
在创建程序集之前,我们先要为它取个名字。
var asmName = new AssemblyName("Test");
AssemblyName位于System.Reflection命名空间下,它代表程序集的名称。
然后我们就可以用上面的名字来创建一个程序集了:
var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
AssemblyBuilderAccess.ReflectionOnly:
DefineDynamicAssembly有很多重载,比如上面的例子可以添加第三个参数用于作为生成的程序集要存放到的目录。关于其他重载形式,大家可以查阅MSDN。这里重点说说AssemblyBuilderAccess这个枚举。
它有以下几个值:
AssemblyBuilderAccess.ReflectionOnly:表示动态程序集只能用于反射获取元素据用,不能执行。
AssemblyBuilderAccess.Run:表示动态程序集是用于执行的。
AssemblyBuilderAccess.Save:表示动态程序集会被保存到磁盘上,不能立即执行。
AssemblyBuilderAccess.RunAndSave:表示动态程序集会被保存至磁盘并能立即执行。
2.创建模块
创建程序集后,就需要为程序集添加模块了,我们可以如下定义一个模块:
var mdlBldr = asmBuilder.DefineDynamicModule("Main", "Main.dll");
如果想把动态生成的程序集保存至磁盘(如本例),定义模块时模块所在文件的名称一定要和保存程序集(后面会提到)时提供的文件名称一样。
3.定义类
有了前面的准备工作,我们开始定义我们的类型:
var typeBldr = mdlBldr.DefineType("Hello",TypeAttributes.Public);
DefineType还可以设置要定义的类的基类,要实现的接口等等。
4.定义类成员(方法,属性等等)
既然有了类,下面我们就为它添加一个SayHello方法吧:
1 var methodBldr = typeBldr.DefineMethod( 2 "SayHello", 3 MethodAttributes.Public, 4 null,//return type 5 null//parameter type 6 );
该方法的原型为public void SayHell();
方法签名已经生成好了,但方法还缺少实现。在生成方法的实现前,必须提及一个很重要的概念:evaluation stack。在.Net下基本所有的操作都是通过入栈出栈完成的。这个栈就是evaluation stack。比如要计算两个数(a,b)的和,首先要将a放入evaluation stack中,然后再将b也放入栈中,最后执行加法时将弹出栈顶的两个元素也就是a和b,相加再将结果推送至栈顶。
Console.WriteLine("Hello,World")可以用Emit这样生成:
1 var il = methodBldr.GetILGenerator();//获取il生成器 2 3 il.Emit(OpCodes.Ldstr,"Hello, World"); 4 5 il.Emit(OpCodes.Call,typeof(Console).GetMethod("WriteLine",new Type[]{typeof(string)})); 6 7 il.Emit(OpCodes.Ret);
OpCodes枚举定义了所有可能的操作,这里用到了:
ldStr:加载一个字符串到evaluation stack。
Call:调用方法。
Ret:返回,当evaluation stack有值时会返回栈顶值。
完成上面的步骤,一个类型好像就已经完成了。事实上却还没有,最后我们还必须显示的调用CreateType来完成类型的创建。
typeBldr.CreateType();
这样一个完整的类就算完成了。但为了能用reflector查看我们创建的动态程序集,我们选择将这个程序集保存下来。
asmBuilder.Save("Main.dll");
如前面定义模块时所说,这里文件名字必须和模块保存到的文件一致,否则我们前面定义的模块和这个模块的一切就无家可归了。接下来,(如果在定义模块时未指定动态创建的程序要保存到哪个目录)我们就可以到 Debug目录下看看生成的Main.dll了,用Reflector打开可以看到:
三、不包含main的控制台程序
一直以来,应用程序(控制台,winform)都是从Main函数启动的,如果没有Main还能启动吗?答案是可以,下面就用emit来做这样一个控制台程序,完整代码如下:
1 var asmName = new AssemblyName("Test"); 2 3 var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( 4 5 asmName, 6 7 AssemblyBuilderAccess.RunAndSave); 8 9 var mdlBldr = asmBuilder.DefineDynamicModule("Main", "Main.exe"); 10 11 var typeBldr = mdlBldr.DefineType("Hello", TypeAttributes.Public); 12 13 var methodBldr = typeBldr.DefineMethod( 14 15 "SayHello", 16 17 MethodAttributes.Public | MethodAttributes.Static, 18 19 null,//return type 20 21 null//parameter type 22 23 ); 24 25 var il = methodBldr.GetILGenerator();//获取il生成器 26 27 il.Emit(OpCodes.Ldstr,"Hello, World"); 28 29 il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[]{typeof(string)})); 30 31 il.Emit(OpCodes.Call, typeof(Console).GetMethod("ReadLine")); 32 33 il.Emit(OpCodes.Pop);//读入的值会被推送至evaluation stack,而本方法是没有返回值的,因此,需要将栈上的值抛弃 34 35 il.Emit(OpCodes.Ret); 36 37 var t = typeBldr.CreateType(); 38 39 asmBuilder.SetEntryPoint(t.GetMethod("SayHello")); 40 41 asmBuilder.Save("Main.exe");