【抬杠.NET】如何进行IL代码的开发(续)-编程思维

背景

之前写了一篇文 【抬杠.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实现

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/huoshan12345/p/16353821.html

.NET C#基础(1):相等性与同一性判定 - 似乎有点小缺陷的设计-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍在C#中的常用的对象比较手段,并提供一些编码上的建议。   1. 阅读基础   1:理解C#基本语法与基本概念(如类、方法、字段与变量声明,知道class引用类型与struct值类型之间存在差异)   2:理解OOP基本概念(如类、对象、方法的概念)

.NET C#基础(2):方法修饰符 - 给方法叠buff-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍C#中的方法修饰符的含义和使用以及注意事项。   1. 阅读基础   理解C#基本语法(如方法声明)   理解OOP基本概念(如多态)   2. 概念:什么是方法修饰符   在C#中,一个方法通常按如下形式声明 [访问修饰符] [方法修饰符] [返回

.NET C#基础(3):事件 - 不便处理的事就委托出去-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍.NET中事件的相关概念、基本知识及其使用方法   1. 阅读基础   理解C#基本语法(方法的声明、方法的调用、类的定义)   2. 从委托说起,到底什么是事件 2.1 方法与委托 (1)从一个案例开始说起   在讨论本节主题之前,我们先来看一个实

.NET C#杂谈(1):变体 - 协变、逆变与不变-编程思维

0. 文章目的:   介绍变体的概念,并介绍其对C#的意义   1. 阅读基础   了解C#进阶语言功能的使用(尤其是泛型、委托、接口)   2. 从示例入手,理解变体   变体这一概念用于描述存在继承关系的类型间的转化,这一概念并非只适用于C#,在许多其他的OOP语言中也都有变体概念。变体一共有三种:协变、逆变与不变

.NET C#基础(6):命名空间 - 组织代码的利器-编程思维

0. 文章目的   面向C#新学者,介绍命名空间(namespace)的概念以及C#中的命名空间的相关内容   1. 阅读基础   理解C与C#语言的基础语法   2. 名称冲突与命名空间 2.1 一个生活例子   假设猫猫头在北京有一个叫AAA的朋友,在上海有两个叫AAA的朋友,上海的两个AAA一个喜欢咸粽子,一个喜

.NET C#基础(1):相等性与同一性判定 - 似乎有点小缺陷的设计-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍在C#中的常用的对象比较手段,并提供一些编码上的建议。   1. 阅读基础   1:理解C#基本语法与基本概念(如类、方法、字段与变量声明,知道class引用类型与struct值类型之间存在差异)   2:理解OOP基本概念(如类、对象、方法的概念)

.NET C#基础(2):方法修饰符 - 给方法叠buff-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍C#中的方法修饰符的含义和使用以及注意事项。   1. 阅读基础   理解C#基本语法(如方法声明)   理解OOP基本概念(如多态)   2. 概念:什么是方法修饰符   在C#中,一个方法通常按如下形式声明 [访问修饰符] [方法修饰符] [返回

.NET C#基础(3):事件 - 不便处理的事就委托出去-编程思维

0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍.NET中事件的相关概念、基本知识及其使用方法   1. 阅读基础   理解C#基本语法(方法的声明、方法的调用、类的定义)   2. 从委托说起,到底什么是事件 2.1 方法与委托 (1)从一个案例开始说起   在讨论本节主题之前,我们先来看一个实

.NET C#杂谈(1):变体 - 协变、逆变与不变-编程思维

0. 文章目的:   介绍变体的概念,并介绍其对C#的意义   1. 阅读基础   了解C#进阶语言功能的使用(尤其是泛型、委托、接口)   2. 从示例入手,理解变体   变体这一概念用于描述存在继承关系的类型间的转化,这一概念并非只适用于C#,在许多其他的OOP语言中也都有变体概念。变体一共有三种:协变、逆变与不变

.NET C#基础(6):命名空间 - 组织代码的利器-编程思维

0. 文章目的   面向C#新学者,介绍命名空间(namespace)的概念以及C#中的命名空间的相关内容   1. 阅读基础   理解C与C#语言的基础语法   2. 名称冲突与命名空间 2.1 一个生活例子   假设猫猫头在北京有一个叫AAA的朋友,在上海有两个叫AAA的朋友,上海的两个AAA一个喜欢咸粽子,一个喜