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