背景
之前寫了一篇文 【抬槓.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的屬性KeyName
,那麼如何為這個屬性賦值呢。
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程式碼,需要指定其構造方法的一個引數skipVisibility
為true
。此外,使用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實現 |