说说emit(上)基本操作
文/玄魂
最近收到《.NET 安全揭秘》的读者的邮件,提到了书中很多大家想看到的内容却被弱化了,我本想回复很多内容因为书的主旨或者章节规划的原因只是概说性的,但是转念一想,读者需要的,不正是作者该写的吗?因此我准备把邮件中的问题一一搬到博客中,以博文的形式分享给大家。
今天要谈论的主题是Emit,反射的孪生兄弟。想要通过几篇博客详尽的讲解Emit也是很困难的事情,本系列计划通过完成一个简单的Mock接口的功能来讲解,计划写三篇博客:
1) 说说Emit(上)基本操作;
2) 说说Emit (中)ILGenerator;
3) 说说Emit (下)Emit在AOP和单元测试中的应用;
这几篇博客不可能涵盖Emit所有内容,只希望能让您知道Emit是什么,有哪些基本功能,如何去使用。
1.1 动态实现接口的技术需求
第一个需要动态实现接口的需求,是我在开发中遇到的,具体的业务场景会在《说说Emit (下) Emit在AOP和单元测试中的应用》中细说,先简要描述代码级别要实现的内容。首先我们有类似图1所示的以Before和After结尾的成对出现的方法若干。
图1 若干成对方法
我们根据一定的规则对上图所示的方法进行分类(分类的规则暂且不提),在实际调用过程中,不会直接调用上面的方法,而是调用一个名为IAssessmentAopAdviceProvider的接口的实例,该接口定义如下:
publicinterfaceIAssessmentAopAdviceProvider
{
object Before(object value);
object After(object beforeResult, object value);
}
负责创建该接口的工厂类定义如下:
staticclassAdviceProviderFactory
{
internalstaticIAssessmentAopAdviceProvider GetProvider(AdviceType adviceType, string instanceName,string funcName,MvcAdviceType mvcAdviceType)
{
//创建接口的实例
}
}
该工厂的职责是根据传入的参数,选择类似图1中的合适的成对方法动态创建一个IAssessmentAopAdviceProvider接口的实例,然后返回供调用方使用。当然如果不使用Emit也能实现这样的需求,这里我们只讨论使用Emit如何实现。
第一个需求简单介绍到这里,我们看第二个需求。现在我要在单元测试中测试某个依赖IAssessmentAopAdviceProvider的类,我们控制IAssessmentAopAdviceProvider的行为该怎么办呢?如果你做过单元测试,一定会想到Mock,我们可以使用Moq:
Mock<IAssessmentAopAdviceProvider> assessmentAopAdviceProviderMocked = newMock<IAssessmentAopAdviceProvider>();
assessmentAopAdviceProviderMocked.Setup(t => t. Before (It.IsAny<object>())).Returns(expectObject);
现在我也想实现这样的功能,该怎么做呢?您先不要惊讶,实现完整的Mock功能要实现一整套动态代理的框架,我还没这个雄心壮志,这里为了演示Emit,我以最简单的方式实现对IAssessmentAopAdviceProvider接口的Before方法的Mock,而且只针对某个特例,只保证这个特例能被调用即可。感兴趣的读者可以去读一读Moq的源码。
OK,技术需求到此结束,下面我们开始动手吧!
1.2 动态创建完整的程序集
终于进入正题了,对于第一个需求,我们要做的工作描述起来很简单,创建一个类,实现IAssessmentAopAdviceProvider接口,期望结果如下:
publicclassAssessmentAopMvcAdviceProvider : IAssessmentAopAdviceProvider
{
publicobject Before(object value = null)
{
MvcAdviceReportProvider.DeleteUserResultBefore(value);
}
publicobject After(object beforeResult, object value = null)
{
MvcAdviceReportProvider.DeleteUserResultAfter(beforeResult ,value);
}
}
上面代码中方法体内部的调用,工厂类会根据规则动态变更,这里我们先只考虑这个特例情况。
首先必要创建类AssessmentAopMvcAdviceProvider,想要创建类型,必要先有模块,想要有模块必须 先有程序集,所以我们要先创建程序集。
(注:下面的创建过程和说明改编自《.NET 安全揭秘》第二章)
先看代码清单2-1。
代码清单2-1 创建程序集
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}
}
}
AppDomain.CurrentDomain.DefineDynamicAssembly方法返回一个AssemblyBuilder实例。其中,第一个参数是AssemblyName实例,是程序集的唯一标识;第二个参数AssemblyBuilderAccess.Run表明该程序集只能用来执行代码,不能被持久保存。AssemblyBuilderAccess还有如下选项:
q AssemblyBuilderAccess.ReflectionOnly:程序集只能在反射上下文中执行。
q AssemblyBuilderAccess.RunAndCollect:程序集可以运行和垃圾回收。
q AssemblyBuilderAccess.RunAndSave:程序集可以执行代码而且被持久保存。
q AssemblyBuilderAccess.Save:程序集是持久化的,保存之前不可以执行代码。
创建了程序集之后,我们继续向程序集中添加模块。
注:“程序集是.NET应用程序的基本单位,是CLR运行托管程序的最基本单位。它通常的表现形式是PE文件,区分PE文件是不是程序集或者说模块和程序集的根本区别是程序集清单,一个PE文件如果包含了程序集清单那么它就是程序集。”----《.NET 安全揭秘》第二章
我们使用如代码清单2-2的方式向程序集中添加模块。
代码清单 2-2
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
}
}
}
在代码清单2-2中,我们使用AssemblyBuilder.DefineDynamicModule 方法来创建模块,该方法共有三个重载,如下表所示:
名称 |
说明 |
定义指定名称的模块。 |
|
定义指定名称的模块,并指定是否发出符号信息。 |
|
定义持久模块。用给定名称定义将保存到指定文件路径的模块。不发出符号信息。 |
|
定义持久模块,并指定模块名称、用于保存模块的文件名,同时指定是否使用默认符号编写器发出符号信息。 |
模块定义完成之后,到了略微关键的一步,定义类型。我们要定义的类型必须继承并实现IAssessmentAopAdviceProvider接口。实现代码如清单2-3。
代码清单2-3
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
}
}
}
上述代码中mb.DefineType方法返回一个TypeBuilder实例,该方法有6个重载方法,这里采用的方法有四个参数,第一个参数是类型名称,第二个参数的TypeAttributes枚举是类型的访问级别和类型类别等其他信息,第三个参数是类型继承的基类,第四个参数是类型实现的接口。其他重载函数的说明如下(引自MSDN):
在此模块中用指定的名称为私有类型构造 TypeBuilder。 |
|
在给定类型名称和类型特性的情况下,构造 TypeBuilder。 |
|
在给定类型名称、类型特性和已定义类型扩展的类型的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和类型的总大小的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和类型的封装大小的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和已定义类型实现的接口的情况下,构造 TypeBuilder。 |
|
DefineType(String, TypeAttributes, Type, PackingSize, Int32) |
在给定类型名称、特性、已定义类型扩展的类型,已定义类型的封装大小和已定义类型的总大小的情况下,构造 TypeBuilder。 |
通过TypeBuilder,可以使用TypeBuilder.DefineField来定义字段,使用TypeBuilder.DefineConstructor来定义构造函数,使用TypeBuilder.DefineMethod来定义方法,并使用TypeBuilder.DefineEvent来定义事件等,总之可以定义类型里的任何成员。这里我们只需要定义方法,如代码清单2-4所示。
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object)});
}
}
}
在上面的代码中,使用TypeBuilder.DefineMethod 方法来创建MethodBuilder对象。该方法有5个重载,如下表(引自MSDN):
名称 |
说明 |
使用指定的名称和方法特性向类型中添加新方法。 |
|
使用指定名称、方法特性和调用约定向类型中添加新方法。 |
|
使用指定的名称、方法特性和调用约定向类型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[]) |
使用指定的名称、方法特性、调用约定和方法签名向类型中添加新方法。 |
使用指定的名称、方法特性、调用约定、方法签名和自定义修饰符向类型中添加新方法。 |
如果需要定义构造函数,可以使用DefineConstructor和DefineDefaultConstructor方法。
在定义了方法之后,还可以使用MethodBuilder.SetSignature方法设置参数的数目和类型。MethodBuilder.SetParameters方法会重写TypeBuilder.DefineMethod 方法中设置的参数信息。当我们的方法接收泛型参数的时候,需要使用MethodBuilder.SetParameters方法来设定泛型参数。
定要了方法,还没有方法体,方法体需要使用ILGenerator类向其中注入il代码。ILGenerator的使用,我们单独放在下一篇博客中,Emit的方法调用的内容会放在第三篇博客中。
现在我们在Main方法中,输出我们刚才创建的程序集的信息,看看创建是否成功。
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitTest.MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object)});
MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), newType[] { typeof(object), typeof(object) });
TestType(typeBuilder);
}
privatestaticvoid TestType(TypeBuilder typeBuilder)
{
Console.WriteLine(typeBuilder.Assembly.FullName);
Console.WriteLine(typeBuilder.Module.Name);
Console.WriteLine(typeBuilder.Namespace);
Console.WriteLine(typeBuilder.Name);
Console.Read();
}
}
此时方法只有定义,还没有方法体,所以还不能创建类型的实例,显示结果如下:
(这里也留给大家一个小问题:为什么上图中输出的模块名称是“在内存模块中”呢?)
1.3 构建工厂类雏形
还记上面提到的工厂类和要实现的目标代码吧,因为还没有描述业务场景,我们先不着急实现它的完整功能,现在不需要它接收任何参数,返回一个特定的IAssessmentAopAdviceProvider接口实例即可。雏形代码如下:
publicstaticclassAdviceProviderFactory
{
staticDictionary<string, IAssessmentAopAdviceProvider> instanceDic;
staticreadonlyAssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
staticAssemblyBuilder assemblyBuilder;
staticModuleBuilder moduleBuilder;
publicstatic AdviceProviderFactory()
{
instanceDic = newDictionary<string, IAssessmentAopAdviceProvider>();
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
}
internalstaticIAssessmentAopAdviceProvider GetProvider()
{
//创建接口的实例
return CreateInstance("MvcAdviceReportProvider");
}
privatestaticIAssessmentAopAdviceProvider CreateInstance(string instanceName)
{
if (instanceDic.Keys.Contains(instanceName))
{
return instanceDic[instanceName];
}
else
{
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitTest.MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object) });
MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), newType[] { typeof(object), typeof(object) });
//todo:注入iL代码,
Type providerType = typeBuilder.CreateType();
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
instanceDic.Add(instanceName, provider);
return provider;
}
}
}
这里只是做了一个简单的封装,没有做过多的其他内容,需要说明的是,通常我们会新建一个新的应用程序域来加载新建的程序集,然后通过透明代理来跨域访问。上面的代码仍然在当前上下文的应用程序域中创建程序集。
架子放在这,会在下一篇博客中,让它切实可用。
1.4 构建Mock类雏形
上面说到Mock类要实现的效果,我们也为它构建一个壳出来。代码如下:
publicclassMock<T> where T : IAssessmentAopAdviceProvider
{
public T Obj {
get { return ConfigObj(this); }
set; }
publicSetupContext Contex { get; set; }
public Mock()
{
Obj = (T)AdviceProviderFactory.GetProvider();
}
private T ConfigObj(Mock<T> mock)
{
returndefault(T);//这里根据SetupContext重新配置方法
}
}
这是一个最简单的Mock,只能用来演示,甚至没任何实际应用价值。其中SetupContext对象用来记录执行Setup和Return扩展方法时的配置信息,定义如下:
publicclassSetupContext
{
publicstring MethodName { get; set; }
publicobject ReturnVlaue { get; set; }
}
此外定义了三个扩展方法,用来配置Mock行为,定义如下:
publicstaticclassMockExtention
{
publicstaticMock<T> Setup<T>(thisMock<T> mocker, Expression<Action<T>> expression)
{
mocker.Contex = newSetupContext();
mocker.Contex.MethodName = expression.ToMethodInfo().Name;
return mocker;
}
publicstaticvoid Returns<T>(thisMock<T> mocker, object returnValue)
{
mocker.Contex.ReturnVlaue = returnValue;
}
publicstaticMethodInfo ToMethodInfo(thisLambdaExpression expression)
{
MemberExpression memberExpression = expression.Body asMemberExpression;
if (memberExpression != null)
{
PropertyInfo propertyInfo = memberExpression.Member asPropertyInfo;
if (propertyInfo != null)
{
return propertyInfo.GetSetMethod(true);
}
}
returnnull;
}
}
现在基本的壳已经有了,后续的实现也不会考虑的太复杂,只根据配置的方法名返回对应的返回值,不会考虑参数对结果的影响。这里把泛型类型约定为IAssessmentAopAdviceProvider,是为了演示方便,可以很方便的扩展为任意类型,不过实现起来也就复杂了。 Mock调用了AdviceProviderFactory来初始化对象的默认值,也就是说在默认情况下会走实际的代码逻辑。现在我们可以按如下方式使用这段代码了:
Mock<IAssessmentAopAdviceProvider> mock = newMock<IAssessmentAopAdviceProvider>();
mock.Setup(t => t.Before(null)).Returns(new { a=""});
到目前为止,我们的准备工作已经完成了,仿佛正题还未开始,是不是太啰嗦了呢?下一篇博客,会专注于ILGenerator,并实现上面的工厂类和Mock类。
出处:https://www.cnblogs.com/xuanhun/archive/2012/06/03/2532922.html
=======================================================================================
说说emit(中)ILGenerator
文/玄魂
在上一篇博客(说说emit(上)基本操作)中,我描述了基本的技术实现上的需求,难度和目标范围都很小,搭建了基本的架子。在代码中实现了程序集、模块、类型和方法的创建,唯一的缺憾是方法体。
方法体是方法内部的逻辑,我们需要将这个逻辑用IL代码描述出来,然后注入到方法体内部。这里自然地引出两个主题,IL代码和用来将Il代码注入到方法体内的工具(ILGenerator)。本篇博客将主要围绕这两个主题展开。但是这一篇博客不可能将IL讲的很详细,只能围绕ILGenerator的应用来讲解。若想了解IL的全貌,我想还是要看ECMA的文档了(http://www.ecma-international.org/publications/standards/Ecma-335.htm)。
2.1 CIL指令简介
这里我们通过几个简单例子来对IL指令有个初步的认识。
新建一个名为“HelloWorld”的控制台项目,代码如清单2-1(虽然在我之前的文章里用过HelloWorld来解释Il,虽然无数篇博客都用过这个例子,但是我还是不厌其烦的用它)。
代码清单2-1 HelloWorld
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World");
}
}
}
编译上面的代码,然后使用ILDasm打开HelloWorld.exe,导出.il文件,内容如下:
// Microsoft (R) .NET Framework IL Disassembler. Version 4.0.30319.1
// Copyright (c) Microsoft Corporation. All rights reserved.
// Metadata version: v4.0.30319
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly HelloWorld
{
//(略)
}
.module HelloWorld.exe
// MVID: {CBB65270-D266-4B29-BAC1-4F255546CDA6}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00020003 // ILONLY 32BITREQUIRED
// Image base: 0x049F0000
// =============== CLASS MEMBERS DECLARATION ===================
.class private auto ansi beforefieldinit HelloWorld.Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Program::Main
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method Program::.ctor
} // end of class HelloWorld.Program
在上面的代码中,隐藏的内容为AssemblyInfo.cs中内容,也就是程序集级别的配置内容。首先注意以”.”开头的字段,.assembly、.module、.class、.method等等,我们称之为CIL指令(CIL Directive)。和指令一同使用的,通常直接跟在指令后面的,称之为CIL 特性(CIL Attributes),上面代码中的 extern,extends、private、public都属于CIL特性,它们的作用是用来描述CIL指令如何被执行。下面先从CIL指令(CIL Directive)的角度看看上面的代码都告诉了我们什么信息。
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
当前程序集引用了程序集mscorlib,该程序集的强名称签名公钥标识为“B7 7A 5C 56 19 34 E0 89”,版本为“4:0:0:0”。
.assembly HelloWorld
{
//(略)
}
定义当前程序集,名称为HelloWorld。
.module HelloWorld.exe
模块为.module HelloWorld.exe。
.imagebase 0x00400000
映像文件基址。
.file alignment 0x00000200
文件对齐大小。
.subsystem 0x0003 // WINDOWS_CUI
指定程序要求的应用程序环境。
.stackreserve 0x00100000
调用堆栈(Call Stack)内存大小。
.corflags 0x00020003 // ILONLY 32BITREQUIRED
保留字段,未使用。
.class private auto ansi beforefieldinit HelloWorld.Program
extends [mscorlib]System.Object
声明类HelloWorld.Program。private是访问类型,auto指明内存布局类型,auto表示内存布局由.NET自动决定(LayoutKind,共有三个值:Sequential,Auto和Explicit),ansi表示在托管和非托管转换时使用的编码类型。extends表示继承。
.method private hidebysig static void Main(string[] args) cil managed
.method,声明方法;private,访问类型;hidebysig,相当于c#方法修饰符new;static,静态方法;void ,返回类型;cil managed,表示托管执行。
.entrypoint
程序入口点。
.maxstack 8
执行方法时的计算堆栈大小。
在方法内部,执行逻辑的编码,被称作操作码(Opcode,Operation Code),如nop,ldstr。操作码也通常被翻译为指令,但是它的英文是Instruction而不是Directive,本文称之为操作指令。完整的操作码速查手册,可参考http://wenku.baidu.com/view/143ab58a6529647d27285234.html。
操作码实际上都是二进制指令,每个指令有其对应的命名,比如操作码0x72对应的名称为ldstr。在操作码前面类似“IL_0000:”这些以冒号结尾的单元是(标签)Label,其值可以任意指定,在执行跳转时会用到Label。
在操作码之前,都会先设置计算堆栈大小。计算堆栈(Evaluation Stack)是用来保存局部变量和方法传人参数的空间。在方法执行前后都要保证计算堆栈为空。
从内存中拷贝数据到计算堆栈的操作称之为Load,以ld开头的操作指令执行的都是load操作,例如ldc.i4为加载一个32位整型数到计算堆栈中,Ldargs.3为将索引为3的参数加载到计算堆栈上。
从计算堆栈拷贝数据回内存的操作为Store,以st开头的操作指令执行的操作都是Store,例如stloc.0为从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中,starg.s为将位于计算堆栈顶部的值存储在参数槽中的指定索引处。
在方法体的开始部分,需要指定在方法执行过程中需要的计算堆栈的最大值,也就是.maxstack指令(directive)。在上面的示例程序中,我们指定最大堆栈值为8,事实上它是编译器指定的默认值。计算运算堆栈的大小最简单的方法是计算方法参数和变量的个数,但是个数往往大于实际需要的堆栈大小。编译器往往会对代码做编译优化,使指定的堆栈大小更合理(最大使用大小)。例如下面的代码
staticvoid Main(string[] args)
{
int v1 = 0;
int v2 = 0;
int v3 = 0;
int v4 = 0;
int v5 = 0;
int v6 = 0;
int v7 = 0;
int v8 = 0;
int v9 = 0;
int v10 = 0;
Console.WriteLine("Hello World");
}
编译之后,编译器设置的计算堆栈为大小为1。
修改成下面的代码之后,计算堆栈的大小是多少呢?
classProgram
{
staticvoid Main(string[] args)
{
int v1 = 0;
int v2 = 0;
int v3 = 0;
int v4 = 0;
int v5 = 0;
int v6 = 0;
int v7 = 0;
int v8 = 0;
int v9 = 0;
int v10 = 0;
UseParams(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10);
Console.WriteLine("Hello World");
}
privatestaticvoid UseParams(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10)
{
int sum = v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10;
}
}
初步统计Main方法的计算堆栈的大小应该是11(变量个数),但是最大使用量是10,所以最终最大计算堆栈的大小应该是10。
其实使用计算堆栈的原则很简单,在使用变量之前将其压栈,使用后弹栈。
这里再啰嗦一句,个人认为学习Il编码的最简单方法是先了解基本原理,准备一份指令表,用C#编写实例代码,然后使用反编译工具反编译查看Il指令,最后再自己模仿编写。
现在我们回头看最简单的HelloWorld程序的内部IL实现。
.entrypoint
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
逐句解释下。
IL_0000: nop
不执行任何push或者pop操作
ldstr "Hello World"
加载字符串"Hello World"的引用到计算堆栈。
call void [mscorlib]System.Console::WriteLine(string)
调用程序集为mscorlib中的System.Console类的方法WriteLine。此时会自动弹出计算堆栈中的值赋值为调用方法的参数。
IL_000c: ret
ret就是return,结束当前方法,返回返回值。
下面我们再来看两个小例子,加深下理解。
staticvoid Main(string[] args)
{
int v1 = 2;
object v2 = v1;
Console.WriteLine((int)v2);
}
这段代码,涉及一个简单的赋值操作和一个装箱拆箱。我们看对应的IL代码:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method
begins at RVA 0x2050
// Code
size 23 (0x17)
.maxstack 1
.entrypoint
.locals init (
[0] int32 v1,
[1] object v2
)
IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox.any [mscorlib]System.Int32
IL_0010: call void [mscorlib]System.Console::WriteLine(int32)
IL_0015: nop
IL_0016: ret
} // end of method
Program::Main
首先是局部变量的声明,IL会在每方法的顶部声明所有的局部变量,使用.locals init。
.locals init ( [0] int32 v1,[1] object v2 )
在示例中声明了v1和v2两个局部变量。事实上这里不仅仅是声明这么简单,这里必须要开辟内存空间, 若要开辟内存空间必须要赋值,也就是说声明的同时要进行赋值,这就是默认值的由来。这个操作就是指令中的init 完成的。更深入的分析,请参考http://blog.liranchen.com/2010/07/behind-locals-init-flag.html。
第一个赋值操作int v1 = 2;是如何完成的呢?
1) ldc.i4.2,加载32位整型数2到计算堆栈;
2) stloc.0,从计算堆栈顶部弹出值赋值到局部变量列表中的第一个变量。
再看第二条语句object v2 = v1的实现过程
1) ldloc.0,加载局部变量列表中的一个变量到计算堆栈中;
2) box [mscorlib]System.Int32,对计算堆栈中的顶部值执行装箱操作
3) stloc.1,从计算堆栈顶部弹出值赋值给局部变量列表中的第二个变量
其他操作类似,就不做解释了。
我们再看一个while循环的操作,了解循环是如何实现的,c#代码如下:
staticvoid Main(string[] args)
{
int i = 0;
while (i < 5)
{
i++;
}
}
对应的Il代码为:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 20 (0x14)
.maxstack 2
.entrypoint
.locals init (
[0] int32 i,
[1] bool CS440000
)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_000b
// loop start (head: IL_000b)
IL_0005: nop
IL_0006: ldloc.0
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: stloc.0
IL_000a: nop
IL_000b: ldloc.0
IL_000c: ldc.i4.5
IL_000d: clt
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: brtrue.s IL_0005
// end loop
IL_0013: ret
} //
end of method Program::Main
首先声明了一个bool类型的局部变量([1] bool CS440000)。在循环开始之前,先执行
IL_0003: br.s IL_000b
跳转到IL_000b处。先加载变量到计算堆栈:
IL_000b: ldloc.0
IL_000c: ldc.i4.5
然执行比较操作:
IL_000d: clt
如果第一个值小于第二值,将整数1压入计算堆栈,否则将整数0压入计算堆栈,同时执行弹栈操作。接下来将比较结果赋值给局部变量列表中的第二个变量:
IL_000f: stloc.1
之后再将局部变量列表中的第二个变量压栈:
IL_0010: ldloc.1
然后判断bool值,确定是否执行循环体内代码:
IL_0011: brtrue.s IL_0005
如果为true,跳转到 IL_0005处,然后继续执行加法操作:
IL_0005: nop
IL_0006: ldloc.0
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: stloc.0
IL_000a: nop
IL_000a: nop执行之后再从 IL_000b处开始新一轮的循环。
关于IL指令的介绍就到这里,不然就收不住笔了,越写越多。现在把思路收回到Emit,当我们了解相关C#代码如何用Il实现之后,下一步就是考虑将Il之类植入方法内部,在Emit的过程中,我们拿什么来表达要植入的Il指令呢?是原生的字符串吗?当然不是,.NET准备了OpCodes 类,该类以字段的形式封装了操作码。
解决了写操作码的问题,下一个问题就是如何发出(Emit)操作码到方法内部了?这就是ILGenerator类。
2.2 ILGenerator
ILGenerator类的功能,一句话,生成IL代码。想要获取ILGenerator类的实例,只有三个途径:
1) ConstructorBuilder.GetILGenerator方法
2) DynamicMethod.GetILGenerator 方法
3) MethodBuilder.GetILGenerator 方法
上面涉及到了在Emit中能够动态生成方法的三种途径,ConstructorBuilder类用来配置的构造函数,构造函数内部的IL要使用它的GetILGenerator方法返回的ILGenerator类发出。DynamicMethod类,是在当前运行上下文环境中动态生成方法的类,使用该类不必事先创建程序集、模块和类型,同样发出其内部的IL使用DynamicMethod.GetILGenerator方法返回的ILGenerator类实例。MethodBuilder我在《说说emit(上)基本操作》中做了介绍,写到这里,突然发现很悲剧的是,竟然没有办法很顺畅的和上篇博客很顺畅的衔接起来。看来写文章也是要讲求设计的。既然无法很好的衔接,也就不强求了,这里将上篇博客提到的示例糅合到一起,实现几个超级简单的Mock接口的例子。
我要实现的调用效果是这样的:
Mock<IAssessmentAopAdviceProvider> mocker = newMock<IAssessmentAopAdviceProvider>();
mocker.Setup(t => t.Before(3)).Returns("HelloWorld!");
Console.WriteLine(mocker.Obj.Before(2));
接收一个接口,初始化一个Mock类的实例,然后通过Setup和Returns扩展方法设定实现该接口的实例在指定方法上的返回值。这里我们先不考虑对不同参数的处理逻辑。
Mock类的定义如下:
publicclassMock<T>
{
public T Obj
{
get;
set;
}
publicSetupContext Contex { get; set; }
public Mock()
{
}
}
Mock类的Obj属性是特定接口的实例。Contex属性是上下文信息,当前内容很简单,只包含一个MethodInfo属性。定义如下:
publicclassSetupContext
{
publicMethodInfo MethodInfo { get; set; }
}
这个上下文信息目前只满足接口有一个方法的情况,对应的相关实现也只考虑一个方法,在这个示例程序中我们无需过分纠结其他细节,以免乱了主次。
接下来是三个扩展方法。
publicstaticclassMockExtention
{
publicstaticMock<T> Setup<T>(thisMock<T> mocker, Expression<Action<T>> expression)
{
mocker.Contex = newSetupContext();
mocker.Contex.MethodInfo = expression.ToMethodInfo();
return mocker;
}
publicstaticvoid Returns<T>(thisMock<T> mocker, object returnValue)
{
if (mocker.Contex != null && mocker.Contex.MethodInfo != null)
{
//这里为简单起见,只考虑IAssessmentAopAdviceProvider接口
mocker.Obj= (T)AdviceProviderFactory.GetProvider(mocker.Contex.MethodInfo.Name,(string)returnValue);
}
}
publicstaticMethodInfo ToMethodInfo(thisLambdaExpression expression)
{
var memberExpression = expression.Body as System.Linq.Expressions.MethodCallExpression;
;
if (memberExpression != null)
{
return memberExpression.Method;
}
returnnull;
}
}
Setup是Mock类的扩展方法,配置要Mock的方法信息;Returns扩展方法则调取对应的工厂获取接口的实例。
ToMethodInfo是LambdaExpression扩展方法,该方法从Lambda表达式中获取MethodInfo。
这里对应的对象工厂也简单化,直接返回IAssessmentAopAdviceProvider接口实例。
首先,在构造函数中,初始化assemblyBuilder和moduleBuilder,代码如下:
static AdviceProviderFactory()
{
assemblyName.Version = newVersion("1.0.0.0");
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProviderModule", "test.dll",true);
}
上面的代码就不解释了,相关内容在前一篇博客有详细的解释。
GetProvider方法当前没有任何逻辑,只是调用了CreateInstance方法。
代码如下:
publicstaticIAssessmentAopAdviceProvider GetProvider(string methodName,string returnValue)
{
//创建接口的实例
return CreateInstance("MvcAdviceReportProviderInstance",methodName,returnValue);
}
CreateInstance方法负责创建类型和方法的实现:
privatestaticIAssessmentAopAdviceProvider CreateInstance(string instanceName,string methodName,string returnValue)
{
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider.MvcAdviceProviderType", TypeAttributes.Public, typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
// typeBuilder.AddInterfaceImplementation(typeof(IAssessmentAopAdviceProvider));
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
beforeMethodBuilder.DefineParameter(1, ParameterAttributes.None ,"value");
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
LocalBuilder local1= generator1.DeclareLocal(typeof(string));
local1.SetLocalSymInfo("param1");
generator1.Emit(OpCodes.Nop);
generator1.Emit(OpCodes.Ldstr, returnValue);
generator1.Emit(OpCodes.Stloc_0);
generator1.Emit(OpCodes.Ldloc_0);
generator1.Emit(OpCodes.Ret);
Type providerType = typeBuilder.CreateType();
assemblyBuilder.Save("test.dll");
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
return provider;
}
在CreateInstance方法中,首先定义类型:
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider.MvcAdviceProviderType", TypeAttributes.Public, typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
注意第三个和第四个参数,分别是该类型的基类型和实现的接口列表。
然后我们根据传人的方法名称和参数定义方法:
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
DefineMethod方法中,传人的第一个参数是方法的名称,第二个参数是访问类型,第三个参数是修饰符,第四个参数是方法的参数类型列表。这里需要注意第二个参数,也就是方法的修饰符,因为接口中的方法定义都是virtual的,所以在实现接口的时候,方法也必须声明为MethodAttributes.Virtual。
接下来定义方法的参数,使用MethodBuilder.DefineParameter方法。
beforeMethodBuilder.DefineParameter(1, ParameterAttributes.None ,"value");
DefineParameter方法的第一参数指定当前定义的参数在方法参数列表中的顺序,从1开始,如果设置为0则代表方法的返回参数。第二个参数是设置参数的特性,如输入参数,输出参数等等。第三个参数是指定该参数的名称。
方法定义完毕,接下来就是发出Opcode,返回一个指定的字符串。先获取ILGenerator实例,如下:
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
我们若想返回一个字符串,必须先为该字符串声明一个局部变量,可以使用LocalBuilder.DeclareLocal方法,如下:
LocalBuilder local1= generator1.DeclareLocal(typeof(string));
local1.SetLocalSymInfo("param1");
ocal1.SetLocalSymInfo("param1")指定局部变量的名称。
需要注意,如果模块定义时不允许发出符号信息,是无法使用SetLocalSymInfo方法的,AdviceProviderFactory的构造函数中,我们定义模块的代码
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProviderModule", "test.dll",true);
最后一个参数是指定是否允许发出符号信息的。
发出的Opcode很简单:
generator1.Emit(OpCodes.Nop);
generator1.Emit(OpCodes.Ldstr, returnValue);
generator1.Emit(OpCodes.Stloc_0);
generator1.Emit(OpCodes.Ldloc_0);
generator1.Emit(OpCodes.Ret);
第一条指令不执行任何操作。
第二条指令加载一个字符串到计算堆栈中。
第三条指令赋值计算堆栈顶部的数据到局部变量列表中的第一个变量。
第四条指令加载局部变量列表中的第一个变量的数据引用到计算堆栈。
第五条指令方法返回。
整个Emit的过程结束了,接下来要创建实例:
Type providerType = typeBuilder.CreateType();
assemblyBuilder.Save("test.dll");
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
在上面的代码中,我们保存了模块,使用反编译工具加载该模块,看看生成的代码是不是预期的。Il代码如下:
class public auto ansi MvcAdviceProvider.MvcAdviceProviderType
extends [mscorlib]System.Object
implements [EmitMock]EmitMock.IAssessmentAopAdviceProvider
{
// Methods
.method public virtual
instance string Before (
int32 'value'
) cil managed
{
// Method begins at RVA 0x2050
// Code size 9 (0x9)
.maxstack 1
.locals init (
[0] string
)
IL_0000: nop
IL_0001: ldstr "HelloWorld!"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ret
} //
end of method MvcAdviceProviderType::Before
.method public specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2068
// Code size 7 (0x7)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} //
end of method MvcAdviceProviderType::.ctor
} // end of class
MvcAdviceProvider.MvcAdviceProviderType
c#代码如下:
using EmitMock;
using System;
namespace MvcAdviceProvider
{
public class MvcAdviceProviderType : IAssessmentAopAdviceProvider
{
public string Before(int value)
{
return "HelloWorld!";
}
}
}
最后,编写一个控制台程序来测试一下:
staticvoid Main(string[] args)
{
EmitMock.Mock<IAssessmentAopAdviceProvider> mocker = newMock<IAssessmentAopAdviceProvider>();
mocker.Setup(t => t.Before(3)).Returns("HelloWorld!");
Console.WriteLine(mocker.Obj.Before(2));
Console.Read();
}
运行结果如下图:
在下一篇博客,不准备继续介绍Emit的应用,在抱怨Emit的繁琐之余,是否还有其他选择呢?我们来谈一谈《Emit和Mono.cecil》。
出处:https://www.cnblogs.com/xuanhun/archive/2012/06/22/2558698.html
=======================================================================================