一, Emit 概述
Emit, 可以称为发出或者产生. 在 Framework 中, 与 Emit 相关的类基本都存在于 System.Reflection.Emit 命名空间下. 可见 Emit 是作为反射的一个元素存在的. 说道反射, 大家应该都不陌生, 它允许我们查看程序集的元素据, 从而取得形如程序集包含哪些类型, 类型包含哪些方法等等大量的信息. 但是反射也仅能够'看', 而 Emit 则可以在运行时动态生成代码. 接下来就来看看如何用 Emit 生成代码.
二, 动态生成代码
首先需要明确的是这里的代码并不是我们时常提到的 C#,VB 等源代码, 而是 IL 代码. 既然是 IL 代码, 那学习 Emit 是不是要先对 IL 很熟悉呢? 诚然, 熟悉 IL 代码对 Emit 学习会大有帮助, 但是不懂也没关系, 因为 IL 和高级语言一样, 也是有一些相对固定的语法结构组成, 不可能在一个 IL 程序里表述 if 是一个样子而到另一个程序却变成了另一个样子. 所以只要多用, 多记, 很快就能掌握这些东西.
其次如 C#,VB 等程序会包含程序集, 模块, 类, 方法, 属性等元素一样, Emit 生成的代码也包括这些元素. 以下介绍 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 方法吧:
- var methodBldr = typeBldr.DefineMethod(
- "SayHello",
- MethodAttributes.Public,
- null,//return type
- null//parameter type );
该方法的原型为 public void SayHell();
方法签名已经生成好了, 但方法还缺少实现. 在生成方法的实现前, 必须提及一个很重要的概念: evaluation stack. 在. Net 下基本所有的操作都是通过入栈出栈完成的. 这个栈就是 evaluation stack. 比如要计算两个数 (a,b) 的和, 首先要将 a 放入 evaluation stack 中, 然后再将 b 也放入栈中, 最后执行加法时将弹出栈顶的两个元素也就是 a 和 b, 相加再将结果推送至栈顶.
Console.WriteLine("Hello,World")可以用 Emit 这样生成:
- var il = methodBldr.GetILGenerator();// 获取 il 生成器
- il.Emit(OpCodes.Ldstr,"Hello, World");
- il.Emit(OpCodes.Call,typeof(Console).GetMethod("WriteLine",new Type[]{
- typeof(string)
- }));
- 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 来做这样一个控制台程序, 完整代码如下:
- var asmName = new AssemblyName("Test");
- var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
- asmName,
- AssemblyBuilderAccess.RunAndSave);
- var mdlBldr = asmBuilder.DefineDynamicModule("Main", "Main.exe");
- var typeBldr = mdlBldr.DefineType("Hello", TypeAttributes.Public);
- var methodBldr = typeBldr.DefineMethod(
- "SayHello",
- MethodAttributes.Public | MethodAttributes.Static,
- null,//return type
- null//parameter type
- );
- var il = methodBldr.GetILGenerator();// 获取 il 生成器
- il.Emit(OpCodes.Ldstr,"Hello, World");
- il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[]{
- typeof(string)
- }));
- il.Emit(OpCodes.Call, typeof(Console).GetMethod("ReadLine"));
- il.Emit(OpCodes.Pop);// 读入的值会被推送至 evaluation stack, 而本方法是没有返回值的, 因此, 需要将栈上的值抛弃
- il.Emit(OpCodes.Ret);
- var t = typeBldr.CreateType();
- asmBuilder.SetEntryPoint(t.GetMethod("SayHello"));
- asmBuilder.Save("Main.exe");
运行生成的 Main.exe 效果如下:
来源: http://www.bubuko.com/infodetail-3013362.html