背景
之前写了一篇文 【抬杠.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代码,需要指定其构造方法的一个参数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实现 |