说说emit
说说emit(上)基本操作
第一个需要动态实现接口的需求,是我在开发中遇到的,具体的业务场景会在《说说Emit (下) Emit在AOP和单元测试中的应用》中细说,先简要描述代码级别要实现的内容。首先我们有类似图1所示的以Before和After结尾的成对出现的方法若干。
图1 若干成对方法
我们根据一定的规则对上图所示的方法进行分类(分类的规则暂且不提),在实际调用过程中,不会直接调用上面的方法,而是调用一个名为IAssessmentAopAdviceProvider的接口的实例,该接口定义如下:
publicinterfaceIAssessmentAopAdviceProvider
object Before(object value);
}
{
{
}
该工厂的职责是根据传入的参数,选择类似图1中的合适的成对方法动态创建一个IAssessmentAopAdviceProvider接口的实例,然后返回供调用方使用。当然如果不使用Emit也能实现这样的需求,这里我们只讨论使用Emit如何实现。
第一个需求简单介绍到这里,我们看第二个需求。现在我要在单元测试中测试某个依赖IAssessmentAopAdviceProvider的类,我们控制IAssessmentAopAdviceProvider的行为该怎么办呢?如果你做过单元测试,一定会想到Mock,我们可以使用Moq:
Mock<IAssessmentAopAdviceProvider> assessmentAopAdviceProviderMocked = newMock<IAssessmentAopAdviceProvider>();
现在我也想实现这样的功能,该怎么做呢?您先不要惊讶,实现完整的Mock功能要实现一整套动态代理的框架,我还没这个雄心壮志,这里为了演示Emit,我以最简单的方式实现对IAssessmentAopAdviceProvider接口的Before方法的Mock,而且只针对某个特例,只保证这个特例能被调用即可。感兴趣的读者可以去读一读Moq的源码。
OK,技术需求到此结束,下面我们开始动手吧!
1.2 动态创建完整的程序集
{
{
}
{
}
上面代码中方法体内部的调用,工厂类会根据规则动态变更,这里我们先只考虑这个特例情况。
首先必要创建类AssessmentAopMvcAdviceProvider,想要创建类型,必要先有模块,想要有模块必须 先有程序集,所以我们要先创建程序集。
(注:下面的创建过程和说明改编自《.NET 安全揭秘》第二章)
using System.Collections.Generic;
using System.Text;
using System.Reflection;
{
{
{
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 安全揭秘》第二章
{
{
{
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}
}
{
{
{
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
}
}
{
{
{
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object)});
}
在上面的代码中,使用TypeBuilder.DefineMethod 方法来创建MethodBuilder对象。该方法有5个重载,如下表(引自MSDN):
|
名称 |
说明 |
|
DefineMethod(String, MethodAttributes) |
使用指定的名称和方法特性向类型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, CallingConventions) |
使用指定名称、方法特性和调用约定向类型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, Type, Type[]) |
使用指定的名称、方法特性和调用约定向类型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[]) |
使用指定的名称、方法特性、调用约定和方法签名向类型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[], Type[], Type[], 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");
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), newType[] { typeof(object), typeof(object) });
}
{
Console.WriteLine(typeBuilder.Module.Name);
Console.WriteLine(typeBuilder.Name);
}
此时方法只有定义,还没有方法体,所以还不能创建类型的实例,显示结果如下:
(这里也留给大家一个小问题:为什么上图中输出的模块名称是“在内存模块中”呢?)
1.3 构建工厂类雏形
{
staticreadonlyAssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
staticModuleBuilder moduleBuilder;
{
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}
{
return CreateInstance("MvcAdviceReportProvider");
privatestaticIAssessmentAopAdviceProvider CreateInstance(string instanceName)
if (instanceDic.Keys.Contains(instanceName))
return instanceDic[instanceName];
else
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitTest.MvcAdviceProvider", TypeAttributes.Public,
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object) });
//todo:注入iL代码,
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
return provider;
}
这里只是做了一个简单的封装,没有做过多的其他内容,需要说明的是,通常我们会新建一个新的应用程序域来加载新建的程序集,然后通过透明代理来跨域访问。上面的代码仍然在当前上下文的应用程序域中创建程序集。
架子放在这,会在下一篇博客中,让它切实可用。
1.4 构建Mock类雏形
{
get { return ConfigObj(this); }
publicSetupContext Contex { get; set; }
{
}
{
}
这是一个最简单的Mock,只能用来演示,甚至没任何实际应用价值。其中SetupContext对象用来记录执行Setup和Return扩展方法时的配置信息,定义如下:
publicclassSetupContext
publicstring MethodName { get; set; }
}
{
{
mocker.Contex.MethodName = expression.ToMethodInfo().Name;
}
{
}
{
if (memberExpression != null)
PropertyInfo propertyInfo = memberExpression.Member asPropertyInfo;
{
}
returnnull;
}
mock.Setup(t => t.Before(null)).Returns(new { a=""});
文/玄魂
在上一篇博客(说说emit(上)基本操作)中,我描述了基本的技术实现上的需求,难度和目标范围都很小,搭建了基本的架子。在代码中实现了程序集、模块、类型和方法的创建,唯一的缺憾是方法体。
方法体是方法内部的逻辑,我们需要将这个逻辑用IL代码描述出来,然后注入到方法体内部。这里自然地引出两个主题,IL代码和用来将Il代码注入到方法体内的工具(ILGenerator)。本篇博客将主要围绕这两个主题展开。但是这一篇博客不可能将IL讲的很详细,只能围绕ILGenerator的应用来讲解。若想了解IL的全貌,我想还是要看ECMA的文档了(http://www.ecma-international.org/publications/standards/Ecma-335.htm)。
2.1 CIL指令简介
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
// Metadata version: v4.0.30319
{
.ver 4:0:0:0
.assembly HelloWorld
//(略)
.module HelloWorld.exe
.imagebase 0x00400000
.stackreserve 0x00100000
.corflags 0x00020003 // ILONLY 32BITREQUIRED
// =============== CLASS MEMBERS DECLARATION ===================
extends [mscorlib]System.Object
.method private hidebysig static void Main(string[] args) cil managed
.entrypoint
.maxstack 8
IL_0001: ldstr "Hello World"
IL_000b: nop
} // end of method Program::Main
instance void .ctor() cil managed
// Code size 7 (0x7)
IL_0000: ldarg.0
IL_0006: ret
} // end of class HelloWorld.Program
{
.ver 4:0:0:0
当前程序集引用了程序集mscorlib,该程序集的强名称签名公钥标识为“B7 7A 5C 56 19 34 E0 89”,版本为“4:0:0:0”。
.assembly HelloWorld
//(略)
定义当前程序集,名称为HelloWorld。
.module HelloWorld.exe
映像文件基址。
.file alignment 0x00000200
指定程序要求的应用程序环境。
.stackreserve 0x00100000
保留字段,未使用。
.class private auto ansi beforefieldinit HelloWorld.Program
声明类HelloWorld.Program。private是访问类型,auto指明内存布局类型,auto表示内存布局由.NET自动决定(LayoutKind,共有三个值:Sequential,Auto和Explicit),ansi表示在托管和非托管转换时使用的编码类型。extends表示继承。
.method private hidebysig static void Main(string[] args) cil managed
程序入口点。
.maxstack 8
{
int v2 = 0;
int v4 = 0;
int v6 = 0;
int v8 = 0;
int v10 = 0;
}
{
{
int v2 = 0;
int v4 = 0;
int v6 = 0;
int v8 = 0;
int v10 = 0;
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;
}
现在我们回头看最简单的HelloWorld程序的内部IL实现。
.entrypoint
.maxstack 8
IL_0001: ldstr "Hello World"
IL_000b: nop
逐句解释下。
IL_0000: nop
加载字符串"Hello World"的引用到计算堆栈。
call void [mscorlib]System.Console::WriteLine(string)
ret就是return,结束当前方法,返回返回值。
下面我们再来看两个小例子,加深下理解。
staticvoid Main(string[] args)
int v1 = 2;
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
在示例中声明了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;
{
}
对应的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 CS4 ">440000
)
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
跳转到IL_000b处。先加载变量到计算堆栈:
IL_000b: ldloc.0
IL_000c: ldc.i4.5
如果第一个值小于第二值,将整数1压入计算堆栈,否则将整数0压入计算堆栈,同时执行弹栈操作。接下来将比较结果赋值给局部变量列表中的第二个变量:
IL_000f: stloc.1
然后判断bool值,确定是否执行循环体内代码:
IL_0011: brtrue.s IL_0005
IL_000a: nop执行之后再从 IL_000b处开始新一轮的循环。
关于IL指令的介绍就到这里,不然就收不住笔了,越写越多。现在把思路收回到Emit,当我们了解相关C#代码如何用Il实现之后,下一步就是考虑将Il之类植入方法内部,在Emit的过程中,我们拿什么来表达要植入的Il指令呢?是原生的字符串吗?当然不是,.NET准备了OpCodes 类,该类以字段的形式封装了操作码。
解决了写操作码的问题,下一个问题就是如何发出(Emit)操作码到方法内部了?这就是ILGenerator类。
2.2 ILGenerator
mocker.Setup(t =>
t.Before(3)).Returns("HelloWorld!");
接收一个接口,初始化一个Mock类的实例,然后通过Setup和Returns扩展方法设定实现该接口的实例在指定方法上的返回值。这里我们先不考虑对不同参数的处理逻辑。
Mock类的定义如下:
publicclassMock
public T Obj
get;
}
public Mock()
}
Mock类的Obj属性是特定接口的实例。Contex属性是上下文信息,当前内容很简单,只包含一个MethodInfo属性。定义如下:
publicclassSetupContext
publicMethodInfo MethodInfo { get; set; }
这个上下文信息目前只满足接口有一个方法的情况,对应的相关实现也只考虑一个方法,在这个示例程序中我们无需过分纠结其他细节,以免乱了主次。
接下来是三个扩展方法。
publicstaticclassMockExtention
publicstaticMock
Setup( thisMock mocker, Expression<Action> expression)
mocker.Contex = newSetupContext();
return mocker;
publicstaticvoid Returns( thisMock
mocker, object returnValue)
if (mocker.Contex != null && mocker.Contex.MethodInfo != null)
//这里为简单起见,只考虑IAssessmentAopAdviceProvider接口
}
publicstaticMethodInfo ToMethodInfo(thisLambdaExpression expression)
var memberExpression = expression.Body as System.Linq.Expressions.MethodCallExpression;
if (memberExpression != null)
return memberExpression.Method;
returnnull;
}
{
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
}
{
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) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
local1.SetLocalSymInfo("param1");
generator1.Emit(OpCodes.Ldstr, returnValue);
generator1.Emit(OpCodes.Ldloc_0);
Type providerType = typeBuilder.CreateType();
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
}
注意第三个和第四个参数,分别是该类型的基类型和实现的接口列表。
然后我们根据传人的方法名称和参数定义方法:
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
DefineParameter方法的第一参数指定当前定义的参数在方法参数列表中的顺序,从1开始,如果设置为0则代表方法的返回参数。第二个参数是设置参数的特性,如输入参数,输出参数等等。第三个参数是指定该参数的名称。
方法定义完毕,接下来就是发出Opcode,返回一个指定的字符串。先获取ILGenerator实例,如下:
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
local1.SetLocalSymInfo("param1");
最后一个参数是指定是否允许发出符号信息的。
发出的Opcode很简单:
generator1.Emit(OpCodes.Nop);
generator1.Emit(OpCodes.Stloc_0);
generator1.Emit(OpCodes.Ret);
assemblyBuilder.Save("test.dll");
在上面的代码中,我们保存了模块,使用反编译工具加载该模块,看看生成的代码是不是预期的。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
最后,编写一个控制台程序来测试一下:
staticvoid Main(string[] args)
EmitMock.Mock<IAssessmentAopAdviceProvider> mocker = newMock<IAssessmentAopAdviceProvider>();
Console.WriteLine(mocker.Obj.Before(2));
}
运行结果如下图:
在下一篇博客,不准备继续介绍Emit的应用,在抱怨Emit的繁琐之余,是否还有其他选择呢?我们来谈一谈《Emit和Mono.cecil》。
出处:https://www.cnblogs.com/xuanhun/archive/2012/06/22/2558698.html
=======================================================================================