分類
發燒車訊

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

目錄

  • 構建代碼
    • 1,程序集(Assembly)
    • 2,模塊(Module)
    • 3,類型(Type)
    • 4,DynamicMethod 定義方法與添加 IL

前面,本系列一共寫了 九 篇關於反射和特性相關的文章,講解了如何從程序集中通過反射將信息解析出來,以及實例化類型。

前面的九篇文章中,重點在於讀數據,使用已經構建好的數據結構(元數據等),接下來,我們將學習 .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 等。

構建代碼

首先我們引入一個命名空間:

using System.Reflection.Emit;

Emit 命名空間中裏面有很多用於構建動態代碼的類型,例如 AssemblyBuilder,這個類型用於構建程序集。類推,構建其它數據結構例如方法屬性,則有 MethodBuilderPropertyBuilder

1,程序集(Assembly)

AssemblyBuilder 類型定義並表示動態程序集,它是一個密封類,其定義如下:

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 下跑。

好了,不再贅述,我們來看看創建一個程序集的代碼:

            AssemblyName assemblyName = new AssemblyName("MyTest");
            AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

構建程序集,分為兩部分:

  • AssemblyName 完整描述程序集的唯一標識。
  • AssemblyBuilder 構建程序集

一個完整的程序集,有很多信息的,版本、作者、構建時間、Token 等,這些可以使用

AssemblyName 來設置。

一般一個程序集需要包含以下內容:

  • 簡單名稱。
  • 版本號。
  • 加密密鑰對。
  • 支持的區域性。

你可以參考以下示例:

            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/645728/what-is-a-module-in-net

模塊是程序集內代碼的邏輯集合,每個模塊可以使用不同的語言編寫,大多數情況下,一個程序集包含一個模塊。程序集包括了代碼、版本信息、元數據等。

MSDN指出:“模塊是沒有 Assembly 清單的 Microsoft 中間語言(MSIL)文件。”。

這些就不再扯淡了。

創建完程序集后,我們繼續來創建模塊。

            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 方法用於創建 classstructDefineEnum方法用於創建 enum

這裏我們分別說明。

創建類或結構體:

TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyClass",TypeAttributes.Public);

定義的時候,注意名稱是完整的路徑名稱,即命名空間+類型名稱。

我們可以先通過反射,獲取已經構建的代碼信息:

            Console.WriteLine($"程序集信息:{type.Assembly.FullName}");
            Console.WriteLine($"命名空間:{type.Namespace} , 類型:{type.Name}");

結果:

程序集信息:MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
命名空間:MyTest , 類型:MyClass

接下來將創建一個枚舉類型,並且生成枚舉。

我們要創建一個這樣的枚舉:

namespace MyTest
{
    public enum MyEnum
    {
        Top = 1,
        Bottom = 2,
        Left = 4,
        Right = 8,
        All = 16
    }
}

使用 Emit 的創建過程如下:

EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEnum", TypeAttributes.Public, typeof(int));

TypeAttributes 有很多枚舉,這裏只需要知道聲明這個枚舉類型為 公開的(Public);typeof(int) 是設置枚舉數值基礎類型。

然後 EnumBuilder 使用 DefineLiteral 方法來創建枚舉。

方法 說明
DefineLiteral(String, Object) 在枚舉類型中使用指定的常量值定義命名的靜態字段。

代碼如下:

            enumBuilder.DefineLiteral("Top", 0);
            enumBuilder.DefineLiteral("Bottom", 1);
            enumBuilder.DefineLiteral("Left", 2);
            enumBuilder.DefineLiteral("Right", 4);
            enumBuilder.DefineLiteral("All", 8);

我們可以使用反射將創建的枚舉打印出來:

        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 方法中調用:

 WriteEnum(enumBuilder.CreateTypeInfo());

接下來,類型創建成員,就複雜得多了。

4,DynamicMethod 定義方法與添加 IL

下面我們來為 類型創建一個方法,並通過 Emit 向程序集中動態添加 IL。這裏並不是使用 MethodBuider,而是使用 DynamicMethod。

在開始之前,請自行安裝反編譯工具 dnSpy 或者其它工具,因為這裏涉及到 IL 代碼。

這裏我們先忽略前面編寫的代碼,清空 Main 方法。

我們創建一個類型:

    public class MyClass{}

這個類型什麼都沒有。

然後使用 Emit 動態創建一個 方法,並且附加到 MyClass 類型中:

            // 動態創建一個方法並且附加到 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 生成一個這樣的方法:

        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

如果你點擊上面的鏈接查看 OpCodes 的枚舉,你可以看到,很多 功能碼,這麼多功能碼是記不住的。我們現在剛開始學習 Emit,這樣就會難上加難。

所以,我們要先下載能夠查看 IL 代碼的工具,方便我們探索和調整寫法。

我們看看此方法生成的 IL 代碼:

  .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 生成工具:

            DynamicMethod dynamicMethod = new DynamicMethod("Add",typeof(int),new Type[] { typeof(int),typeof(int)});
            ILGenerator ilCode = dynamicMethod.GetILGenerator();

DynamicMethod 用於定義一個方法;ILGenerator是 IL 生成器。當然也可以將此方法附加到一個類型中,完整代碼示例如下:

            // 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<int, int, int> test = (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
            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 編寫有所差異,具體俺也不知道為啥,在群里問了調試了,註釋掉那麼幾行代碼,才通過的。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益