说说emit


说说emit()基本操作

第一个需要动态实现接口的需求,是我在开发中遇到的,具体的业务场景会在《说说Emit () EmitAOP和单元测试中的应用》中细说,先简要描述代码级别要实现的内容。首先我们有类似图1所示的以BeforeAfter结尾的成对出现的方法若干。

 

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[][])

使用指定的名称、方法特性、调用约定、方法签名和自定义修饰符向类型中添加新方法。

如果需要定义构造函数,可以使用DefineConstructorDefineDefaultConstructor方法。

在定义了方法之后,还可以使用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对象用来记录执行SetupReturn扩展方法时的配置信息,定义如下:

   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.Programprivate是访问类型,auto指明内存布局类型,auto表示内存布局由.NET自动决定(LayoutKind,共有三个值:SequentialAutoExplicit),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

在示例中声明了v1v2两个局部变量。事实上这里不仅仅是声明这么简单,这里必须要开辟内存空间, 若要开辟内存空间必须要赋值,也就是说声明的同时要进行赋值,这就是默认值的由来。这个操作就是指令中的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类的实例,然后通过SetupReturns扩展方法设定实现该接口的实例在指定方法上的返回值。这里我们先不考虑对不同参数的处理逻辑。

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的繁琐之余,是否还有其他选择呢?我们来谈一谈《EmitMono.cecil》。

出处:https://www.cnblogs.com/xuanhun/archive/2012/06/22/2558698.html

=======================================================================================

C