程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> emit的ILGenerator

emit的ILGenerator

編輯:關於.NET

在上一篇博客(說說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代碼:

.methodprivatehidebysigstatic
void Main (
string[] args
) cilmanaged
{
// Methodbegins at RVA 0x2050
// Codesize 23 (0x17)
.maxstack 1
.entrypoint
.localsinit (
[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 methodProgram::Main

首先是局部變量的聲明,IL會在每方法的頂部聲明所有的局部變量,使 用.locals init。

.localsinit ( [0] int32 v1,[1] object v2 )

在示例中聲明了v1和v2兩個局部變量。事實上這裡不僅僅是聲明這麼簡單,這 裡必須要開辟內存空間,若要開辟內存空間必須要賦值,也就是說聲明的同時要 進行賦值,這就是默認值的由來。這個操作就是指令中的init 完成的。更深入的 分析,請參考http://blog.liranchen.com/2010/07/behind-locals-init- flag.html。

第一個賦值操作int v1 = 2;是如何完成的呢?

2)       stloc.0,從計算堆棧頂部彈出值賦值到局部變量 列表中的第一個變量。

再看第二條語句object v2 = v1的實現過程

2)       box [mscorlib]System.Int32,對計算堆棧中的頂 部值執行裝箱操作

1)       ConstructorBuilder.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

 {

     publicMethodInfoMethodInfo { get; set; }

}

這個上下文信息目前只滿足接口有一個方法的情況,對應的相關實現也只考慮 一個方法,在這個示例程序中我們無需過分糾結其他細節,以免亂了主次。

接下來是三個擴展方法。

publicstaticclassMockExtention

 {

     publicstaticMock<T>Setup<T> (thisMock<T> mocker, Expression<Action<T>> expression)

     {

        mocker.Contex = newSetupContext();

        mocker.Contex.MethodInfo = expression.ToMethodInfo();

        returnmocker;

     }

     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)

     {

        varmemberExpression = expression.Body as System.Linq.Expressions.MethodCallExpression;

;

        if(memberExpression != null)

        {

            returnmemberExpression.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)

{

TypeBuildertypeBuilder = moduleBuilder.DefineType ("MvcAdviceProvider.MvcAdviceProviderType", TypeAttributes.Public, typeof(object), newType[] { typeof (IAssessmentAopAdviceProvider) });

//typeBuilder.AddInterfaceImplementation(typeof (IAssessmentAopAdviceProvider));

MethodBuilderbeforeMethodBuilder = 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);

TypeproviderType = typeBuilder.CreateType();

assemblyBuilder.Save("test.dll");

IAssessmentAopAdviceProvider provider = Activator.CreateInstance (providerType) asIAssessmentAopAdviceProvider;

returnprovider;

}

查看本欄目

在上面的代碼中,我們保存了模塊,使用反編譯工具加載該模塊,看看生成的 代碼是不是預期的。Il代碼如下:

classpublicautoansi MvcAdviceProvider.MvcAdviceProviderType
extends [mscorlib]System.Object
implements [EmitMock] EmitMock.IAssessmentAopAdviceProvider
{
// Methods
.methodpublicvirtual
instancestring Before (
int32 'value'
) cilmanaged
{
// Method begins at RVA 0x2050
// Code size 9 (0x9)
.maxstack 1
.localsinit (
[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

.methodpublicspecialnamertspecialname
instancevoid.ctor () cilmanaged
{
// Method begins at RVA 0x2068
// Code size 7 (0x7)
.maxstack 2

IL_0000: ldarg.0
IL_0001: call instancevoid [mscorlib] System.Object::.ctor()
IL_0006: ret
} //end of method MvcAdviceProviderType::.ctor

} // end of classMvcAdviceProvider.MvcAdviceProviderType

c#代碼如下:

using EmitMock;
using System;
namespace MvcAdviceProvider
{
public class MvcAdviceProviderType : IAssessmentAopAdviceProvider
{
public string Before (intvalue)
{
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》。

作者:玄魂

出處:http://www.cnblogs.com/xuanhun/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved