或者保存到磁盘上。处于这个原因,许多的.net编译器使用了 System.Reflection.Emit来提供代码生成。在这片文章中,我们将探索下
System.Reflection.Emit命名空间,并构建一个简单的类库程序集
System.Reflection.Emit命名空间概览
类名: 描述
AssemblyBuilder 用来动态创建程序集
ModuleBuilder 用来动态创建模块
TypeBuilder 用来动态创建类型
FieldBuilder 用来动态创建字段
ConstructorBuilder 用来动态创建构造函数
MethodBuilder 用来动态创建方法
ILGenerator 用来生成il指令到动态创建的方法或构造函数中
OpCodes 包含il指令的类
Label (struct) 用来定义标签(用于il分支跳转)
另外还有一些还有一些其他的类,本文并没有用到。
类名: 描述:
CustomAttributeBuilder 用来创建自定义特性
EnumBuilder 用来创建枚举
EventBuilder 用来创建事件
LocalBuilder 用来创建方法和构造函数用到的局部变量
MethodRental 用来交换方法的实现
ParameterBuilder 用来创建方法和构造函数用到的参数
PropertyBuilder 用来创建类型的属性
SignatureHelper 用来创建字段,方法,属性和局部签名
UnmanagedMarshal 用来描述托管到非托管类型的封送
现在我们已经对System.Reflection.Emit命名空间有了一个大概印象,让我们使用这些类来构建一个简单的类库程序集,然后保存到磁
盘中,最后在另一个程序中使用动态创建的程序集
类库程序集代码
这个我们将在运行时创建的类库提供了一个简单的类。这个类可以让我们从文件中读取一些警句名言,然后可以随机的取出名言。代码如下
{
public class QuoteOfTheDay
{
private System.Collections.ArrayList m_Quotes = new System.Collections.ArrayList();
private Random m_Random = new Random();
public QuoteOfTheDay(string filename)
{
//根据文件名打开一个text reader
System.IO.TextReader tr = System.IO.File.OpenText(filename);
string quote;
// 将每一行的名言加入到数组链表中
while ( (quote = tr.ReadLine()) != null)
m_Quotes.Add(quote);
// 关闭 text reader
tr.Close();
}
public string GetRandomQuote()
{
int count = m_Quotes.Count;
// 如果没有任何名言返回一个空字符串
if (count == 0)
return "";
//否则从数组链表中返回一个随机的名言
return (string) m_Quotes[ m_Random.Next(count) ];
}
}
}
可以看到这个类从文件中加载了所有的名言(每行一个),然后通过GetRandomQuote方法返回一个随机的名言。了解这个类后。我们
将在运行时生成这个类,而非直接编译这段代码。
创建动态程序集和模块
这里我们将使用 "System.Reflection" 和"System.Reflection.Emit" 命名空间。我们可以很容易的通过调用 AppDomain.DefineDynamicAssembly() (有9个 重载)方法来创建动态程序集,下面我们将创建一个能够保存到磁盘的弱名称的程序集
// 版本号 1.0.0.0
an.Version = new Version(1, 0, 0, 0);
// 设置程序集名称
an.Name = "QuoteOfTheDay";
// 定义动态程序集
AssemblyBuilder ab = System.AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Save);
以免有些人不熟悉程序集名称,有两种方法来命名程序集。1.弱名称,允许定义版本信息,文本名称,文化信息。2.强名称程序集,比弱
名称多了一个公钥和数字签名。强名称程序集保证了名称的唯一性,而弱名称不能保证唯一性。更多关于强名称的信息请参考 msdn上"Strong-Named Assemblies"
注意我们的程序集有"Save"访问权限。也就是说我们可以保存动态程序集到硬盘上。(如果是"Run"的话只能运行动态程序集不能保存到硬盘上,“RunAndSave”则保存运行都允许)现在我们有了动态程序集,下面就可以创建动态模块了。这里我将创建一个单模块程序集,所以我们的模块名称和程序集名一样。
定义一个动态模块,我们将调用AssemblyBuilder.DefineDynamicModule():
// 定义一个动态模块(名称和程序集一样)
ModuleBuilder modBuilder = ab.DefineDynamicModule("QuoteOfTheDay", "QuoteOfTheDay.dll");
这个动态创建的模块将被保存为 QuoteOfTheDay.dll。默认程序集将被保存为动态链接库(dll)。要想生成可执行文件,必须调用
AssemblyBuilder.SetEntryPoint()设置程序入口的方法 。调用这个方法允许你生成控制台或者windows应用程序程序集
现在有了动态程序集和模块,下面就可以做些有意思的了。我们将定义在运行时定义QuoteOfTheDay 类型
创建动态类型
跟在动态程序集中定义动态模块一样,我们可以在动态模块中定义动态类型(这里指QuoteOfTheDay )
TypeBuilder tb = modBuilder.DefineType("QuoteOfTheDay.QuoteOfTheDay", TypeAttributes.Class | TypeAttributes.Public);
TypeAttributes允许我们制定类的访问修饰符(private,public,等)。当然也包括sealed,abstract等关键字。现在我们已经有了TypeBuilder对象,下面就可以使用它来构造我们的QuoteOfTheDay类了
动态定义字段
从上面类的c#代码可以看出。类有两个私有字段 m_Quotes 和 m_Random 我们可以在我们的QuoteOfTheDay类中定义这两个字段,代码如下
FieldBuilder m_Random = tb.DefineField("m_Random", typeof(System.Random), FieldAttributes.Private);
TypeBuilder.DefineField方法接受三个参数 字段名称, 类型 , 特性(可见性..).下面开始定义类的构造函数
定义动态类型的构造函数
如果你不了解 msil也不用担心,下面的代码将非常简单
ConstructorBuilder cb = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard,
new Type[] { typeof(String) });
//获取构造函数的IL 生成器对象
ILGenerator ilgen = cb.GetILGenerator();
// 定义局部变量tr
ilgen.DeclareLocal(typeof(System.IO.TextReader));
// 定义局部变量quote
ilgen.DeclareLocal(typeof(System.String));
// 加载 "this"指针到运行栈上
ilgen.Emit(OpCodes.Ldarg_0);
// 调用基类(Object)的默认无参构造函数
ilgen.Emit(OpCodes.Call, typeof(Object).GetConstructor(new Type[0]));
// 加载 "this"指针到运行栈上
ilgen.Emit(OpCodes.Ldarg_0);
// 创建新的ArrayList对象
ilgen.Emit(OpCodes.Newobj, typeof(System.Collections.ArrayList).GetConstructor(new Type[0]));
// 保存ArrayList对象到字段字段 "m_Quotes"
ilgen.Emit(OpCodes.Stfld, m_Quotes);
// 加载this
ilgen.Emit(OpCodes.Ldarg_0);
// 创建新的Random对象
ilgen.Emit(OpCodes.Newobj, typeof(System.Random).GetConstructor(new Type[0]));
// 保存Random对象到字段 "m_Random"
ilgen.Emit(OpCodes.Stfld, m_Random);
// 加载 "filename"参数到栈上
ilgen.Emit(OpCodes.Ldarg_1);
// 调用File.OpenText(string)方法
ilgen.Emit(OpCodes.Call, typeof(System.IO.File).GetMethod("OpenText"));
// 保存结果到局部变量 "tr"
ilgen.Emit(OpCodes.Stloc_0);
// 定义 loop and exit 标签(用于我们的while循环)
Label loop = ilgen.DefineLabel();
Label exit = ilgen.DefineLabel();
//标记 loop 标签
ilgen.MarkLabel(loop);
// 加载局部变量 "tr"到栈上
ilgen.Emit(OpCodes.Ldloc_0);
// 调用tr.ReadLine() 虚方法
ilgen.Emit(OpCodes.Callvirt, typeof(System.IO.TextReader).GetMethod("ReadLine"));
// 存储结果到局部变量"quote"到栈上
ilgen.Emit(OpCodes.Stloc_1);
// 加载局部变量"quote"到栈上
ilgen.Emit(OpCodes.Ldloc_1);
// "quote" 是null 跳转到exit标签
ilgen.Emit(OpCodes.Brfalse_S, exit);
// 加载this到栈上
ilgen.Emit(OpCodes.Ldarg_0);
// 加载"m_Quotes" 字段到栈上
ilgen.Emit(OpCodes.Ldfld, m_Quotes);
// 加载局部变量 "quote"到栈上
ilgen.Emit(OpCodes.Ldloc_1);
// 调用m_Quotes.Add(object) 虚方法
ilgen.Emit(OpCodes.Callvirt, typeof(System.Collections.ArrayList).GetMethod("Add"));
//从运行栈弹出 m_Quotes.Add(object)调用结果,因为没用到
ilgen.Emit(OpCodes.Pop);
// 跳转到loop标签
ilgen.Emit(OpCodes.Br_S, loop);
// 标记exit标签
ilgen.MarkLabel(exit);
//加载局部变量 "tr"
ilgen.Emit(OpCodes.Ldloc_0);
// 调用tr.Close() 虚方法
ilgen.Emit(OpCodes.Callvirt, typeof(System.IO.TextReader).GetMethod("Close"));
// 返回
ilgen.Emit(OpCodes.Ret);
让我们检查下上面都干了些啥,首先我们使用 TypeBuilder.DefineConstructor()定义了一个public构造函数 ,接受一个字符串参数。一旦有了ConstructorBuilder,我们通过 ConstructorBuilder.GetILGenerator()获取了这个构造函数的IL生成器对象。IL生成器允许我们给构造函数生成msil指令。
我们然后定义了两个局部变量tr(TextReader类型),quote(String类型)。因为我们不打算生成调试符号,所以这两个变量都没有命名。如果要生成调试符合,可以调用AssemblyBuilder.DefineDynamicModule()的重载方法,使用LocalBuilder.SetLocalSymInfo()(LocalBuilder是由 TypeBuilder.DeclareLocal()返回的)
下面我们调用基类Object的构造函数。注意我们在调用基类构造函数前首先加载了第一个参数(OpCodes.Ldarg_0)到运行栈上。回忆下对象的方法,第一个隐藏的参数this是一个指向当前被调用对象的引用。传给QuoteOfTheDay 构造函数的字符串参数实际上是第二个参数。(使用OpCodes.Ldarg_1加载)
我们接着初始化了一个ArrayList对象(通过调用ArrayList的默认构造函数).将它保存到m_Quotes字段。然后创建了Random对象,保存到m_Random字段。
下面我定义了两个标签,标签将用来构建我们的while循环。loop标签是循环的开始,exit标签是循环的结束。这样就告诉了IL生成器将标签和标签紧跟的的指令偏移量关联(用于指令跳转)。下面的代码是我们的while循环的条件部分。我们调用tr.ReadLine()然后存储结果(读取文件的一行)到局部变量到quote.如果quote是null就跳转到exit。(指令brfalse.s 表示如果运行栈顶值是0, 跳转到标签关联的代码位置)
如果quote不是null,调用m_Quotes.Add()将quote添加到链表中。注意Add方法后面的pop指令,Add方法会返回一个整数(标识新添项的索引)到运行栈上。如果我们不用它,必须将它弹出栈(否则函数返回时栈是非空,会抛出异常)。接着是无条件跳转到loop标签关联的指令接着循环。循环结束后最后返回。现在构造函数生成完了,下面接着生成 GetRandomQuote()
动态定义类型的方法
MethodBuilder mb = tb.DefineMethod("GetRandomQuote", MethodAttributes.Public, CallingConventions.Standard,
typeof(System.String), new Type[0]);
// Get the IL generator for the method
ilgen = mb.GetILGenerator();
// Declare the "count" local
ilgen.DeclareLocal(typeof(int));
// Define the cont label
Label cont = ilgen.DefineLabel();
// Load "this"
ilgen.Emit(OpCodes.Ldarg_0);
// Load field "m_Quotes"
ilgen.Emit(OpCodes.Ldfld, m_Quotes);
// Call m_Quotes.get_Count() (virtual)
ilgen.Emit(OpCodes.Callvirt, typeof(System.Collections.ArrayList).GetMethod("get_Count"));
// Store in local "count"
ilgen.Emit(OpCodes.Stloc_0);
// Load local "count"
ilgen.Emit(OpCodes.Ldloc_0);
// Branch if count is not 0
ilgen.Emit(OpCodes.Brtrue_S, cont);
// Load the string ""
ilgen.Emit(OpCodes.Ldstr, "");
// Return
ilgen.Emit(OpCodes.Ret);
// Mark the cont label
ilgen.MarkLabel(cont);
// Load "this"
ilgen.Emit(OpCodes.Ldarg_0);
// Load field "m_Quotes"
ilgen.Emit(OpCodes.Ldfld, m_Quotes);
// Load "this"
ilgen.Emit(OpCodes.Ldarg_0);
// Load field "m_Random"
ilgen.Emit(OpCodes.Ldfld, m_Random);
// Load local "count"
ilgen.Emit(OpCodes.Ldloc_0);
// Call m_Random.Next(int) (virtual)
ilgen.Emit(OpCodes.Callvirt, typeof(System.Random).GetMethod("Next", new Type[] { typeof(int) }));
// Call m_Quotes.get_Item(int) (virtual)
ilgen.Emit(OpCodes.Callvirt, typeof(System.Collections.ArrayList).GetMethod("get_Item"));
// Cast the result to string
ilgen.Emit(OpCodes.Castclass, typeof(System.String));
// Return
ilgen.Emit(OpCodes.Ret);
下面分析下上面代码,首先定义了一个public 的方法GetRandomQuote,然后通过MethodBuilder.GetILGenerator()获取这个方法的
IL生成器对象.下面就开始生成方法体。首先定义一个int型局部变量(对应c#代码中的count),,然后定义一个标签cont ,如果if返回false的话就
接着cont关联指令继续执行。然后将字段 m_Quotes 加载到运行栈上,调用ArrayList.get_Count 方法。回忆下get_xxx,set_xxx会映射到c#的xxx属性上。
下面我们实现if语句,通过将count变量加载到运行栈上执行brtrue.s指令(如果count是非零,则跳转到指定标签关联指令(cont)
,否则接着执行后面的指令)。接着的另个指令是加载一个空字符串到运行栈然后返回
如果count是非零,程序会跳转到cont标记的指令运行。首先加载m_Quotes到栈上,然后加载m_Random到栈上,下面调用
m_Random.Next方法。这个方法返回一个随机数放到栈上,作为 m_Quotes.get_Item的参数。
然后调用m_Quotes.get_Item(注意get_Item是索引器),返回的结果被转型为字符串。最后返回
我们的类型构建完毕。现在我们就可以创建这个类型并保存程序集了
创建动态类型和保存动态程序集
在我们保存动态程序集到硬盘前,我们必须创建QuoteOfTheDay 类型。我们只要简单的调用TypeBuilder.CreateType()就ok了,代码如下
// "Bake" the QuoteOfTheDay type
tb.CreateType();
生成最终的类型这一步骤是必须的。如果在保存动态程序集之前不调用CreateType(),就会出现
System.NotSupportedException 告诉我们QuoteOfTheDay 类型不完整。下面就可以调用AssemblyBuilder.Save()方法保存程序集了
// Save the assembly to the file specified
ab.Save("QuoteOfTheDay.dll");
好了终于完成了,如果你运行上面的代码,就会发现当前目录下多了一个新的QuoteOfTheDay.dll文件。你可以使用il反汇编器
(ildasm.exe)反汇编这个dll查看生成的il代码来验证是否正确。
使用生产的程序集
我们可以创建一个控制台程序,添加对QuoteOfTheDay.dll的引用。Main方法中写如下代码
QuoteOfTheDay.QuoteOfTheDay qotd = new QuoteOfTheDay.QuoteOfTheDay("quotes.txt");
Console.WriteLine("Random Quote Of The Day: " + qotd.GetRandomQuote());
运行程序就会输出随机的名言了。
结论
本文我们学会了如何创建动态程序集,模块,类型。 System.Reflection.Emit命名空间提供了非常简单干净的方式来生成代码,我
们就不用学习复杂的PE文件格式了。所以如果感兴趣的话你可以为你最喜欢的语言写个.net编译器。
原文:http://devhood.com/Tutorials/tutorial_details.aspx?tutorial_id=238