首頁‎ > ‎電子期刊‎ > ‎2010年2月號‎ > ‎

.NET Emit技術初探(上)


在本文中說明.NET Emit技術,簡單的來說Emit的技術可以在執行時期動態的建立Assembly、Class,是.NET提供相當彈性的技術。

立Assembly


 
private static void GenerateAssemblyAndModule()
{
AssemblyBuilder asmBuilder = null;
ModuleBuilder modBuilder = null;
 
if (asmBuilder == null)
{
AssemblyName assemblyName = new AssemblyName("DynamicUDT");
 
AppDomain thisDomain = Thread.GetDomain();
 
asmBuilder = thisDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
 
modBuilder = asmBuilder.DefineDynamicModule(asmBuilder.GetName().Name, false);
}
}

要動態的建立Assembly有三個步驟,第一是要建立AssemblyName物件,AssemblyName用來表達Assembly的名稱, 其有參數的建構式只有一個,亦即傳入Assembly的名字;AssemblyName還有一些有用的屬性,例如可用Version來取得 Assembly的版本編號。

接下來取得目前執行的AppDomain以用來建立Assembly,要取得AppDomain有兩種方法,第一種是透過 Thread.GetDomain()方法來取得,另外一種方法則是用AppDomain.CurrentDomain屬性來取得;AppDomain簡 單的說可以是當成是應用程式執行的環境,可以包含多個Assembly。

在取得AppDomain後即可呼叫DefineDynamicAssembly來動態建立Assembly,只需傳入AssemblyName物件;並且指定執行型態,分為四種:

  • ReflectionOnly:動態Assembly已載入,只能供Reflection使用。
  • Save:建立的Assembly只能儲存,不能執行。
  • Run:建立的Assembly只能執行,不能儲存。
  • SaveAndRun:建立的Assembly能夠儲存也能夠執行。

在有了Assembly的實體後,接下來建立Module,Module(模組)是諸如type.dll或application.exe 等類型可移植的執行檔,包含一個或多個類別 (Class) 和介面。多個命名空間可能包含在單一模組中,並且命名空間可以擴展多個模組。

有個重要的觀念要說明,呼叫DefineDynamicAssembly會傳回AssemblyBuilder物件,而可用 AssemblyBuilder來呼叫DefineDynamicModule方法取得ModuleBuilder;也就是說我們運用Emit來定義一個 動態的實體,通常是呼叫Define相關的方法,而會傳回對應的Builder類別,而可再用Builder類別再去仔細定義其下的內容。

理解這樣的觀念對於Emit要熟悉就輕鬆多了。

建立Class

取得了ModuleBuilder後,終於可以運用ModuleBuilder來建立類別,我們呼叫ModuleBuilder的 DefineType來建立類別;第一個參數是類別名稱,第二個參數是類別的屬性,第三個參數是此動態類別所繼承的類別;另外可傳入第四個參數為實作的介 面列表。

 
private static TypeBuilder CreateType(ModuleBuilder modBuilder, string typeName)
{
TypeBuilder typeBuilder = modBuilder.DefineType(typeName,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
typeof(FISCA.UDT.ActiveRecord));
 
return typeBuilder;
}

下面為最常用的DefineType方法,亦即上述提到可傳入四個參數。

 
public TypeBuilder DefineType(
string name,
TypeAttributes attr,
Type parent,
Type[] interfaces
)

此處要注意的是若是要取得一般物件的型別(System.Type)可用GetType()方法,但是若本身就是型別,要取得其系統型別,那麼要用關鍵字typeof(型別)來取得,以供Reflection及Emit所使用。

建立Constructor

在取得了TypeBuilder物件後,即可用此物件來建立建構式,雖然物件屬性被標示為AutoClass會自動建立建構式;但是面對像Emit 這樣底層的技術,我會建議還是走過一次如何建立建構式的過程,而且若是有繼承類別,並且在父類別的建構式中有程式碼要執行,那麼最好也是熟過自行建立建構 式,以確保會呼叫到父代的類別。

我們看到以下的程式碼,先呼叫DefineConstructor建立建構式,傳入建構式的屬性,留意在最後一個參數傳入Type.EmptyTypes代表是無參數建構式,或是可以傳入型別代表建構式的傳入參數。

接下來的程式碼做仔細的拆解,其中typeBuilder.BaseType是取得繼承的父類別型態,再用GetConstructor取得父類別的建構式實體,傳入的參數為Type.EmptyTypes,亦即父代也是空白建構式。

需要注意的觀念是DefineConstructor是用來"定義建構式",而GetConstructor則是用來取得"可實際被呼叫的建構式方法",若是這觀念清楚,相信Emit與Reflection的觀念也就相當清楚。

接下來透過constructor取得其ILGenerator,亦即透過ILGenerator物件可以實際定義中介語言的內容,重點在 於定義的第二行呼叫il.Emit(OpCodes.Call,conObj);亦即直接呼叫剛才我們取得的父代Constructor方法,用這樣的方 式可以確保是呼叫到正確的父代建構式。

 
private static void CreateConstructor(TypeBuilder typeBuilder)
{
ConstructorBuilder constructor = typeBuilder.DefineConstructor(
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName,
CallingConventions.Standard,
Type.EmptyTypes);
 
ConstructorInfo conObj = typeBuilder.BaseType.GetConstructor(Type.EmptyTypes);
 
//call constructor of base object
 

ILGenerator il = constructor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, conObj);
il.Emit(OpCodes.Ret);
}

建立Property

在上段我們建立了建構式,接下來我們要建立類別的欄位;雖然類別的屬性是AutoClass會自動產生欄位,不過我的建議是使用Emit這種接近底層的技術,最好實際走過一次細節,若是要自行詳細定義屬可分為三部份:

  • 建立屬性。
  • 建立屬性的存取欄位。
  • 建立屬性存取欄位方法。

我們首先看到前兩點,在語法上都不難,一樣的我們都是呼叫TypeBuilder的變數來建立;下列程式碼第一行是建立屬性,第二行是建立欄位。

 
//第一個參數是屬性名稱,第二個參數是屬性的標籤,第三個參數是傳回的參數型別,第四個參數是傳入的參數型別。
PropertyBuilder PBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None , typeof(string), null);
 
//第二個參數是欄位名稱,第二個參數是欄位的型別,第三個參數是欄位的標籤,其中標識為私有(Private)欄位。
FieldBuilder FBuilder = typeBuilder.DefineField("m_Name",typeof(string),FieldAttributes.Private);

.NET Emit技術初探(下)
Comments