期刊/.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);
