【擡槓.NET】如何進行IL程式碼的開發(續)

2022-06-08 09:01:03

背景

之前寫了一篇文 【擡槓.NET】如何進行IL程式碼的開發 介紹了幾種IL程式碼的開發方式。

  • 建立IL專案
  • C#專案混合編譯IL
  • 使用InlineIL.Fody
  • 使用DynamicMethod(ILGenerator)

我個人比較喜歡IL和C#在同一個專案的方式(畢竟單單為了一點點IL程式碼新建一個IL專案也挺麻煩的),所以一直在用InlineIL.Fody。後來在使用過程中發現了一些它的限制,而如果轉而使用混合編譯的方式呢,又無法對C#程式碼進行debug了(因為最終的pdb檔案實際上是根據IL原始碼生成的)。
因此,我使用Fody編寫了一個外掛,叫做MixedIL.Fody,徹底解決了這些問題。

 

InlineIL.Fody的一個限制:如何為無公共setter的自動屬性賦值

AssemblyKeyNameAttribute為例,這是.Net類庫裡的一個特性。它有個無公共setter的屬性Name,那麼如何為這個屬性賦值呢。


namespace System.Reflection
{
    [AttributeUsage(AttributeTargets.Assembly, Inherited = false)]
    public sealed class AssemblyKeyNameAttribute : Attribute
    {
        public AssemblyKeyNameAttribute(string keyName)
        {
            KeyName = keyName;
        }
 
        public string KeyName { get; }
    }
}

我們知道,自動屬性會有個編譯器生成的欄位。所以可以用反射獲取到該欄位,然後賦值即可,如下:

var attribute = new AssemblyKeyNameAttribute("name");
var field = typeof(AssemblyKeyNameAttribute).GetField("<KeyName>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
field.SetValue(attribute, "newName");

那如果不用反射呢?可以使用IL程式碼實現:

.class public abstract sealed auto ansi beforefieldinit System.ObjectExtensions
{
    .method public hidebysig static void SetKeyName(class [System.Runtime]System.Reflection.AssemblyKeyNameAttribute attribute, string keyName) cil managed
    {
        .maxstack 8
        ldarg.0
        ldarg.1
        stfld string [System.Runtime]System.Reflection.AssemblyKeyNameAttribute::'<KeyName>k__BackingField'
        ret
    }
}

上面的IL程式碼相當於實現了一個靜態方法:

public static class ObjectExtensions
{
    public static void SetKeyName(AssemblyKeyNameAttribute attribute, string keyName);
}

 所以用InlineIL.Fody實現如下:

public static void SetKeyName(AssemblyKeyNameAttribute attribute, string keyName)
{
    IL.Emit.Ldarg(nameof(attribute));
    IL.Emit.Ldarg(nameof(keyName));
    IL.Emit.Stfld(FieldRef.Field(TypeRef.Type<AssemblyKeyNameAttribute>(), "<KeyName>k__BackingField"));
}

然而編譯的時候會報錯,Fody/InlineIL: Field '<KeyName>k__BackingField' not found。原因在於AssemblyKeyNameAttribute雖然是個公共類,但是和上面寫的SetKeyName方法不在同一個程式集,而私有欄位在跨程式集存取時會多一些額外的限制(反射沒有這方面的限制)。例如,如果使用DynamicMethod實現上述IL程式碼,需要指定其構造方法的一個引數skipVisibilitytrue。此外,使用Expression甚至無法繞過改限制。使用IL程式碼依然有這個限制,下一節會介紹如何繞過。

 

實現MixedIL.Fody

MixedIL.Fody是一款基於Fody的外掛,其原理很簡單,就是使用MSBuild增加編譯步驟:用Microsoft.NETCore.ILAsm編譯IL程式碼檔案,然後將這步生成的dll內的各個方法的il指令填充到C#程式碼生成的dll內即可。相比上篇文章裡介紹的混合編譯,使用這個這種方法,專案內C#程式碼也可以正常偵錯。該外掛的使用方法可以參考MixedIL.Fody的專案介紹。

上一節的需求可以使用此類庫實現如下:

  • 編寫C#函數樁,無方法體。
using System.Reflection;
using MixedIL;

namespace System;

public static class ObjectExtensions
{
    [MixedIL]
    public static extern void SetKeyName(this AssemblyKeyNameAttribute attribute, string keyName);
}
  • 在這個專案內,建立一個.il檔案,將上節中的il程式碼寫入這個檔案。
  • il程式碼存取其他程式集的私有欄位也需要繞開限制,所以還需要為該程式集增加一個特性[assembly: IgnoresAccessChecksTo("System.Private.CoreLib")]如果不加這個特性執行時會報錯 。而IgnoresAccessChecksToAttribute這個特性已經包含在MixedIL.Fody內了。
  • 最後編譯這個程式集即可。

這個例子可以在這裡找到:MixedIL.Example

 

總結

本文由一個InlineIL.Fody的限制,引出了MixedIL.Fody這個類庫的建立動機和介紹。

最後我重新總結一下IL開發的各種方法的優缺點。

方法 優點 缺點 應用場景
建立IL專案 原生IL 建立的時候較為複雜 較多程式碼需IL實現
C#專案混合編譯IL 原生IL 無法偵錯專案內的C#程式碼 少量方法需IL實現
使用InlineIL.Fody 純C#編寫體驗 某些場景不支援 少量方法需IL實現
使用DynamicMethod 執行時生成程式碼,靈活 效能有損耗,需快取一些物件 需執行時生成程式碼
使用MixedIL.Fody 原生IL - 少量方法需IL實現