1. 程式人生 > >C# 反射與特性(十):EMIT 構建程式碼

C# 反射與特性(十):EMIT 構建程式碼

[TOC] 前面,本系列一共寫了 九 篇關於反射和特性相關的文章,講解了如何從程式集中通過反射將資訊解析出來,以及例項化型別。 前面的九篇文章中,重點在於讀資料,使用已經構建好的資料結構(元資料等),接下來,我們將學習 .NET Core 中,關於動態構建程式碼的知識。 其中表達式樹已經在另一個系列寫了,所以本系列主要是講述 反射,Emit ,AOP 等內容。 如果現在總結一下,反射,與哪些資料結構相關? 我們可以從 AttributeTargets 列舉中窺見: ``` public enum AttributeTargets { All=16383, Assembly=1, Module=2, Class=4, Struct=8, Enum=16, Constructor=32, Method=64, Property=128, Field=256, Event=512, Interface=1024, Parameter=2048, Delegate=4096, ReturnValue=8192 } ``` 分別是程式集、模組、類、結構體、列舉、建構函式、方法、屬性、欄位、事件、介面、引數、委託、返回值。 以往的文章中,已經對這些進行了很詳細的講解,我們可以中反射中獲得各種各樣的資訊。當然,我們也可以通過動態程式碼,生成以上資料結構。 動態程式碼的其中一種方式是表示式樹,我們還可以使用 Emit 技術、Roslyn 技術來編寫;相關的框架有 Natasha、CS-Script 等。 ## 構建程式碼 首先我們引入一個名稱空間: ```csharp using System.Reflection.Emit; ``` Emit 名稱空間中裡面有很多用於構建動態程式碼的型別,例如 `AssemblyBuilder`,這個型別用於構建程式集。類推,構建其它資料結構例如方法屬性,則有 `MethodBuilder`、`PropertyBuilder` 。 ### 1,程式集(Assembly) AssemblyBuilder 型別定義並表示動態程式集,它是一個密封類,其定義如下: ```csharp public sealed class AssemblyBuilder : Assembly ``` AssemblyBuilderAccess 定義動態程式集的訪問模式,在 .NET Core 中,只有兩個列舉: | 列舉 | 值 | 說明 | | ------------- | ---- | ------------------------------------------------------------ | | Run | 1 | 可以執行但無法儲存該動態程式集。 | | RunAndCollect | 9 | 當動態程式集不再可供訪問時,將自動解除安裝該程式集,並回收其記憶體。 | .NET Framework 中,有 RunAndSave 、Save 等列舉,可用於儲存構建的程式集,但是在 .NET Core 中,是沒有這些列舉的,也就是說,Emit 構建的程式集只能在記憶體中,是無法儲存成 .dll 檔案的。 另外,程式集的構建方式(API)也做了變更,如果你百度看到文章 `AppDomain.CurrentDomain.DefineDynamicAssembly`,那麼你可以關閉建立了,說明裡面的很多程式碼根本無法在 .NET Core 下跑。 好了,不再贅述,我們來看看建立一個程式集的程式碼: ```csharp AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ``` 構建程式集,分為兩部分: - AssemblyName 完整描述程式集的唯一標識。 - AssemblyBuilder 構建程式集 一個完整的程式集,有很多資訊的,版本、作者、構建時間、Token 等,這些可以使用 AssemblyName 來設定。 一般一個程式集需要包含以下內容: - 簡單名稱。 - 版本號。 - 加密金鑰對。 - 支援的區域性。 你可以參考以下示例: ```csharp AssemblyName assemblyName = new AssemblyName("MyTest"); assemblyName.Name = "MyTest"; // 建構函式中已經設定,此處可以忽略 // Version 表示程式集、作業系統或公共語言執行時的版本號. // 建構函式比較多,可以選用 主版本號、次版本號、內部版本號和修訂號 // 請參考 https://docs.microsoft.com/zh-cn/dotnet/api/system.version?view=netcore-3.1 assemblyName.Version = new Version("1.0.0"); assemblyName.CultureName = CultureInfo.CurrentCulture.Name; // = "zh-CN" assemblyName.SetPublicKeyToken(new Guid().ToByteArray()); ``` 最終程式集的 AssemblyName 顯示名稱是以下格式的字串: ``` Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision> <, StrongName> <,PublicKeyToken> '\0' ``` 例如: ``` ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d015c7d5a0b012 ``` 另外,建立程式集構建器使用 `AssemblyBuilder.DefineDynamicAssembly()` 而不是 `new AssemblyBuilder()` 。 ### 2,模組(Module) 程式集和模組之間的區別可以參考 [https://stackoverflow.com/questions/9271805/net-module-vs-assembly](https://stackoverflow.com/questions/9271805/net-module-vs-assembly) [https://stackoverflow.com/questions/645728/what-is-a-module-in-net](https://stackoverflow.com/questions/645728/what-is-a-module-in-net) 模組是程式集內程式碼的邏輯集合,每個模組可以使用不同的語言編寫,大多數情況下,一個程式集包含一個模組。程式集包括了程式碼、版本資訊、元資料等。 MSDN指出:“模組是沒有 Assembly 清單的 Microsoft 中間語言(MSIL)檔案。”。 這些就不再扯淡了。 建立完程式集後,我們繼續來建立模組。 ```csharp AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule("MyTest"); // ⬅ ``` ### 3,型別(Type) 目前步驟: ``` Assembly -> Module -> Type 或 Enum ``` ModuleBuilder 中有個 `DefineType` 方法用於建立 `class` 和 `struct`;`DefineEnum`方法用於建立 `enum`。 這裡我們分別說明。 建立類或結構體: ```csharp TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyClass",TypeAttributes.Public); ``` 定義的時候,注意名稱是完整的路徑名稱,即名稱空間+型別名稱。 我們可以先通過反射,獲取已經構建的程式碼資訊: ```csharp Console.WriteLine($"程式集資訊:{type.Assembly.FullName}"); Console.WriteLine($"名稱空間:{type.Namespace} , 型別:{type.Name}"); ``` 結果: ``` 程式集資訊:MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null 名稱空間:MyTest , 型別:MyClass ``` 接下來將建立一個列舉型別,並且生成列舉。 我們要建立一個這樣的列舉: ```csharp namespace MyTest { public enum MyEnum { Top = 1, Bottom = 2, Left = 4, Right = 8, All = 16 } } ``` 使用 Emit 的建立過程如下: ```csharp EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEnum", TypeAttributes.Public, typeof(int)); ``` TypeAttributes 有很多列舉,這裡只需要知道宣告這個列舉型別為 公開的(Public);`typeof(int)` 是設定列舉數值基礎型別。 然後 EnumBuilder 使用 `DefineLiteral` 方法來建立列舉。 | 方法 | 說明 | | ----------------------------- | ------------------------------------------------ | | DefineLiteral(String, Object) | 在列舉型別中使用指定的常量值定義命名的靜態欄位。 | 程式碼如下: ```csharp enumBuilder.DefineLiteral("Top", 0); enumBuilder.DefineLiteral("Bottom", 1); enumBuilder.DefineLiteral("Left", 2); enumBuilder.DefineLiteral("Right", 4); enumBuilder.DefineLiteral("All", 8); ``` 我們可以使用反射將建立的列舉打印出來: ```csharp public static void WriteEnum(TypeInfo info) { var myEnum = Activator.CreateInstance(info); Console.WriteLine($"{(info.IsPublic ? "public" : "private")} {(info.IsEnum ? "enum" : "class")} {info.Name}"); Console.WriteLine("{"); var names = Enum.GetNames(info); int[] values = (int[])Enum.GetValues(info); int i = 0; foreach (var item in names) { Console.WriteLine($" {item} = {values[i]}"); i++; } Console.WriteLine("}"); } ``` Main 方法中呼叫: ```csharp WriteEnum(enumBuilder.CreateTypeInfo()); ``` 接下來,型別建立成員,就複雜得多了。 ### 4,DynamicMethod 定義方法與新增 IL 下面我們來為 型別建立一個方法,並通過 Emit 向程式集中動態新增 IL。這裡並不是使用 MethodBuider,而是使用 DynamicMethod。 在開始之前,請自行安裝反編譯工具 dnSpy 或者其它工具,因為這裡涉及到 IL 程式碼。 這裡我們先忽略前面編寫的程式碼,清空 Main 方法。 我們建立一個型別: ```csharp public class MyClass{} ``` 這個型別什麼都沒有。 然後使用 Emit 動態建立一個 方法,並且附加到 MyClass 型別中: ```csharp // 動態建立一個方法並且附加到 MyClass 型別中 DynamicMethod dyn = new DynamicMethod("Foo",null,null,typeof(MyClass)); ILGenerator iLGenerator = dyn.GetILGenerator(); iLGenerator.EmitWriteLine("HelloWorld"); iLGenerator.Emit(OpCodes.Ret); dyn.Invoke(null,null); ``` 執行後會列印字串。 DynamicMethod 型別用於構建方法,定義並表示可以編譯、執行和丟棄的一種動態方法。 丟棄的方法可用於垃圾回收。。 ILGenerator 是 IL 程式碼生成器。 EmitWriteLine 作用是列印字串, OpCodes.Ret 標記 結束方法的執行, Invoke 將方法轉為委託執行。 上面的示例比較簡單,請認真記一下。 下面,我們要使用 Emit 生成一個這樣的方法: ```csharp public int Add(int a,int b) { return a + b; } ``` 看起來很簡單的程式碼,要用 IL 來寫,就變得複雜了。 ILGenerator 正是使用 C# 程式碼的形式去寫 IL,但是所有過程都必須按照 IL 的步驟去寫。 其中最重要的,便是 OpCodes 枚舉了,OpCodes 有幾十個列舉,代表了 IL 的所有操作功能。 請參考:[https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1](https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1) 如果你點選上面的連結檢視 OpCodes 的列舉,你可以看到,很多 功能碼,這麼多功能碼是記不住的。我們現在剛開始學習 Emit,這樣就會難上加難。 所以,我們要先下載能夠檢視 IL 程式碼的工具,方便我們探索和調整寫法。 我們看看此方法生成的 IL 程式碼: ```csharp .method public hidebysig instance int32 Add( int32 a, int32 b ) cil managed { .maxstack 2 .locals init ( [0] int32 V_0 ) // [14 9 - 14 10] IL_0000: nop // [15 13 - 15 26] IL_0001: ldarg.1 // a IL_0002: ldarg.2 // b IL_0003: add IL_0004: stloc.0 // V_0 IL_0005: br.s IL_0007 // [16 9 - 16 10] IL_0007: ldloc.0 // V_0 IL_0008: ret } // end of method MyClass::Add ``` 看不懂完全沒關係,因為筆者也看不懂。 目前我們已經獲得了上面兩大部分的資訊,接下來我們使用 `DynamicMethod` 來動態編寫方法。 定義 Add 方法並獲取 IL 生成工具: ```csharp DynamicMethod dynamicMethod = new DynamicMethod("Add",typeof(int),new Type[] { typeof(int),typeof(int)}); ILGenerator ilCode = dynamicMethod.GetILGenerator(); ``` DynamicMethod 用於定義一個方法;ILGenerator是 IL 生成器。當然也可以將此方法附加到一個型別中,完整程式碼示例如下: ```csharp // typeof(Program),表示將此動態編寫的方法附加到 MyClass 中 DynamicMethod dynamicMethod = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) },typeof(MyClass)); ILGenerator ilCode = dynamicMethod.GetILGenerator(); ilCode.Emit(OpCodes.Ldarg_0); // a,將索引為 0 的自變數載入到計算堆疊上。 ilCode.Emit(OpCodes.Ldarg_1); // b,將索引為 1 的自變數載入到計算堆疊上。 ilCode.Emit(OpCodes.Add); // 將兩個值相加並將結果推送到計算堆疊上。 // 下面指令不需要,預設就是彈出計算堆疊的結果 //ilCode.Emit(OpCodes.Stloc_0); // 將索引 0 處的區域性變數載入到計算堆疊上。 //ilCode.Emit(OpCodes.Br_S); // 無條件地將控制轉移到目標指令(短格式)。 //ilCode.Emit(OpCodes.Ldloc_0); // 將索引 0 處的區域性變數載入到計算堆疊上。 ilCode.Emit(OpCodes.Ret); // 即 return,從當前方法返回,並將返回值(如果存在)從被呼叫方的計算堆疊推送到呼叫方的計算堆疊上。 // 方法1 Func test = (Func)dynamicMethod.CreateDelegate(typeof(Func)); Console.WriteLine(test(1, 2)); // 方法2 int sum = (int)dynamicMethod.Invoke(null, BindingFlags.Public, null, new object[] { 1, 2 }, CultureInfo.CurrentCulture); Console.WriteLine(sum); ``` 實際以上程式碼與我們反編譯出來的 IL 編寫有所差異,具體俺也不知道為啥,在群裡問了除錯了,註釋掉那麼幾行程式碼,才通過的。