郑兴旺 (中国矿业大学计算机系 江苏徐州 221008)
【摘要】.NET平台的出现提高了程序的执行效率,本文就以类加计算为例,解释了动态代码的生成和使用。并就Emit命名空间中的一些类做了简要的介绍。
【关键词】 组装 模块 接口 类型
.NET平台的出现大大简化了我们的程序开发以及程序发布工作,比如对于私有的组装(Assembly,组装是.NET最基本的部署单元)可以通过简单的拷贝来安装。这是因为在.NET的PE文件中包含了自己的类型数据信息。为了对这些信息进行处理,.NET基础类库提供了System.Reflection命名空间。该空间提供了进行元数据读取、类型检查以及类型动态激活等功能的类,同时该命名空间的子空间Emit中提供了在运行期进行组装、模块、类或者方法的动态生成和使用。本文将通过对类的动态生成来介绍Emit的用法及带来的好处。
在某些场合下,我们可能需要根据用户的需要动态生成一些代码。由于这些代码需要在运行期生成,所以我们不能象通常那样对该代码文件进行编译。在以前的编程模式下,我们可能需要做这样的处理:首先打开一个文件,然后将代码内容输入到该文件中,然后调用命令行将其转化为库文件。接下来再调用该库文件中的函数,最后删除该文件。在.NET中,我们也可以做类似的处理。
1 组装、模块和类的生成
考虑下面的例子,我们进行简单的加法运算。该方法接收一个参数,然后完成从1到该数的累加。这样的代码实现起来非常简单,只需要一个循环即可。即:
public int DoSum (int n)
{
int result = 0;
for(int i = 1;i <= n; i++)
{
result += i;
}
return result;
}
但是,如果采用下面的方法计算(假使输入的数为20):
public int DoSum ( )
{
return 1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17+18+19+20;
}
很显然,由于减少了循环变量的自加,以及每一次循环的比较,代码的执行效率将大大提高。但是我们怎样才能根据用户不同的输入生成对应的代码呢?在这里我们不采用使用硬盘文件作为缓冲的办法,而是采用.NET提供的基础类库System.Reflection.Emit提供的功能来动态生成组装、模块、类型以及类型中包含的方法。
由于.NET中支持完全的面向对象编程,所以任何方法都必须存在于某一个类中。为了创建一个能够被调用的内存库文件,我们首先要建立一个动态组装。为此,我们首先创建一个Assembly对象,该对象将代表这个动态生成的组装。这个工作是由AssemblyBuilder来完成的。为了使用AssemblyBuilder来创建组装,首先需要建立一个AssemblyName对象(该类属于System.Reflection命名空间),用于为组装进行命名。如下:
AssemblyName assemblyName=new AssemblyName();
AssemblyName.Name=”DoSumAssembly”;
然后我们就可以使用这个AssemblyName对象来创建动态组装。在.NET中,一个执行的程序称为一个进程,在进程内,根据不同的安全规则分为不同的应用域,真正执行的线程存在于应用域当中。所以建立组装时,首先获取当前应用域,然后通过应用域的成员方法建立组装。如下
AssemblyBuilder dyAssembly=
Thread.GetDomain().DefineDynamicAssembly(myAssemblyName,AssemblyBuilderAccess.Run);
AssemblyBuilderAccess是一个枚举类型,包含了Run、RunAndSave以及Save,分别代表创建的组装可以执行、执行或者保存、只能保存。由于我们要使用该组装中包含的方法进行计算,所以选择Run。
由于每一个类都存在于某一个模块中,所以我们需要使用ModuleBuilder来构造模块,过程与构造组装类似。
ModuleBuilder dyModule=dyAssembly.DefineDynamicModule(“DoSumModule”);
接下来我们就可以建造类了。我们可以建造一个公有类及其实现计算的方法,然后再通过动态激活来使用它。但是为了方便,我们定义一个接口,然后让该类实现这个接口,调用的时候只需要将该类的实例转化为接口类型即可。下面是接口的定义。
public interface Isum
{
int DoSum();
}
然后我们采用TypeBuilder构造一个类,并实现Isum接口。
TypeBuilder dySumClass=dyModule.DefineType(“MySumClass”,TypeAttributes.Public);
dySumClass.AddInterfaceImplemention(typeof(ISum));
2 实现计算方法的动态生成
最后我们实现计算的功能,该方法通过MethodBuilde构造,由TypeBuilder类的DefineMethod方法来返回一个TypeBuilder实例。如下:
MethodBuilder doSumMethod=
dySumClass.DefineMethod(“DoSum”,MethodAttributes.Public|MethodAttributes.Virtual,
returnType,paramTypes);
其中的returnType,paramTypes分别表示该方法的返回值和参数,返回值是Type类型,在这里我们定义:
Type returnType=typeof(int);
而paramTypes由于可以由多个参数,所以采用一个Type数组,在这里没有任何参数,所以我们定义一个零维的数组:
Type paramTypes=new Type[0];
在这里,我们完成了组装、模块、类以及其方法的定义。我们来看如何使用ILGenerator实现代码的输出。ILGenerator是.NET Reflection Emit中进行代码输出的类,该类包含了对动态输出程序的代码和结构的控制。比如定义变量、定义块、对变量进行各种运算等。
该类的最主要的代码输出方法是其Emit方法,该方法带有两个参数,分别代表运算的类型和参与运算的数据。运算的类型由一个OpCode结构类型表示,在OpCodes类中定义了一系列OpCode结构类型的只读属性,分别表示不同的运算,如Sub_Ovf_Un表示进行无符号类型数的除法,并进行溢出检查。
为了完成我们的DoSum方法,我们需要使用doSumMethod对象来构造,首先通过其GetILGenerator方法得到一个中间代码生成器
ILGenerator doSumGenerator=doSumMethod.GetILGenerator();
然后使用该代码生成器生成累计相加的方法,我们首先看三个OpCodes类的成员。这三个成员都是OpCode结构类型。
Ldc_I4将后面的32位整形操作数压如堆栈,Emit方法的第而个参数是一个int类型;Add将堆栈中的两个数值成员弹出,并进行加法运算,然后将结果压入堆栈。
Ret从被调者的堆栈中取出一个数据并压入调用者的堆栈空间,即相当于把从方法的堆栈返回一个数据作为返回值。
我们采用这两个运算类型可以完成我们的累加代码生成工作。这部分的代码如下:
ILGenerator.Emit(OpCodes.Ldc_I4, 0);
for (int i = 1; i <= theValue;i++)
{
ILGenerator.Emit(OpCodes.Ldc_I4, i);
ILGenerator.Emit(OpCodes.Add);
}
我们看到,首先将0、1两个数压如堆栈,然后取出进行加法运算,然后再将结果压入,依此类推,完成从1到theValue的累加运算。
在程序中,theValue可以由用户输入,所以我们可以根据用户输入的不同产生不同的代码。最后我们将该方法的结果返回
ILGenerator.Emit(OpCodes.Ret);
我们需要说明该方法实现接口Isum的DoSum方法。所以我们需要建立二者的重载关系,为此,我们首先获取Isum.DoSum方法的信息。
MethodInfo IdoSumInfo=typeof(Isum).GetMethod(“DoSum”);
然后设置该类对接口Isum中DoSum方法的重载。
dySumClass.DefineMethodOverride(doSumMethod, IdoSumInfo);
最后,我们创建该类。
dySumClass.createType();
3 动态生成代码的运行
要使用我们创建的组装,我们首先要生成其包含的类MySumClass的实例,并将其转化为Isum接口类型,最后调用该接口的doSum方法。
Isum theDoSumInterface=(Isum)dyAssembly.createInstance(“dyAssembly”);
TheDoSumInterface.doSum();
4 小节
从本文所提供的例子可以看出,在.NET中可以使用Reflection Emit动态的生成组装、模块、类型(包括类、接口、代理等)以及方法的实现。我们可以采用该命名空间中提供的类和方法生成动态代码来提高程序效率,甚至可以实现我们自己的中间代码(IL)编译器。