当前位置: 首页 > news >正文

如何快速编写和调试 Emit 生成 IL 的代码

.NET Core/.NET Framework 的 System.Reflection.Emit 命名空间为我们提供了动态生成 IL 代码的能力。利用这项能力,我们能够在运行时生成一段代码/一个方法/一个类/一个程序集。

大家都知道反射的性能很差,通过缓存反射调用的方法则能够大幅提升性能。Emit 为我们提供了这项能力,我们能够在运行时生成一段代码,替代使用反射动态调用的代码,以提升性能。


我们在解决什么问题?

之前我写过一篇创建委托以大幅度提高反射调用的性能的方法,不过此方法适用于预先知道方法参数和返回值类型的情况。如果我们在编译期不知道类型,那么它就行不通了。(原因?注意到那篇文章中返回的委托有类型强转吗?也就是说需要编译期确定类型,即便是泛型。)

例如,我们在运行时得到一个对象,希望为这个对象的部分或全部属性赋值;此对象的类型和属性类型在编译期全部不可知(就算是泛型也没有)。

class SomeClass
{
    [DefaultValue("walterlv")]
    public string SomeProperty { get; set; }
}

众所周知的反射能够完成这个目标,但它不是本文讨论的重点;因为一旦这样的方法会被数万数十万甚至更多次调用的时候,反射将造成性能灾难。

既然反射不行,通过反射的创建委托也不行,那还有什么方法?

  1. 使用表达式树(不是本文重点)
  2. 使用 Emit(本文)

如果事先不能知道类型,那么只能每次通过反射去动态的调用,于是才会耗费大量的性能。如果我们能够在运行时动态地生成一段调用方法,那么这个调用方法将可以缓存下来供后续重复调用。如果我们使用 Emit,那么生成的方法与静态编写的代码是一样的,于是就能获得普通方法的性能。

为了实现动态地设置未知类型未知属性的值,我决定写出如下方法:

static void SetPropertyValue(object @this, object value)
{
    ((类的类型) @this).属性名称 = (属性的类型) value;
}

不用考虑编译问题了,这段代码是肯定编译不过的。方法是一个静态方法,传入两个参数——类型的实例和属性的新值;方法内部为实例中某个属性赋新值。

类的类型、属性名称和属性的类型是编译期不能确定,但可以在运行时确定的;如果此生成的方法会被大量调用,那么性能优势将极其明显。

快速编写 Emit

为了快速编写和调试 Emit,我们需要 ReSharper 全家桶:

  • ReSharper - 用于实时查看 IL 代码
  • dotPeek - 免费,用于查看我们使用 Emit 生成的代码,便于对比分析

相比于原生 Visual Studio,有此工具帮助的情况下,IL 的编写速度和调试速度将得到质的提升。(当然,利用这些工具依然只是手工操作,存在瓶颈;如果你阅读完本文之后找到或编写一个新的工具,更快,欢迎与我探讨。)

ReSharper 提供了 IL Viewer 窗格,从菜单依次进入 ReSharper->Windows->IL Viewer 可以打开。

ReSharper IL Viewer

打开后立即可以看到我们当前正在编写的代码的 IL,而且还能高亮光标所在的代码块。(如果你的 IL Viewer 中没有代码或没有高亮,编译一遍项目即可。)

IL Viewer 中的代码与编写的代码对应

我们要做的,就是得知 SetPropertyValue 在编译后将得到什么样的 IL 代码,这样我们才能编写出正确的 IL 生成代码来。于是编写这些辅助代码:

namespace Walterlv.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            var instance = new TempClass();
            SetPropertyValue(instance, "test");
        }

        static void SetPropertyValue(object @this, object value)
        {
            ((TempClass) @this).TempProperty = (string) value;
        }
    }

    public class TempClass
    {
        public string TempProperty { get; set; }
    }
}

编译之后去 IL Viewer 中看 SetPropertyValue 的 IL 代码:

.method private hidebysig static void 
    SetPropertyValue(
        object this, 
        object 'value'
    ) cil managed 
{
    .maxstack 8

    // [14 9 - 14 10]
    IL_0000: nop          

    // [15 13 - 15 63]
    IL_0001: ldarg.0      // this
    IL_0002: castclass    Walterlv.Demo.TempClass
    IL_0007: ldarg.1      // 'value'
    IL_0008: castclass    [System.Runtime]System.String
    IL_000d: callvirt     instance void Walterlv.Demo.TempClass::set_TempProperty(string)
    IL_0012: nop          

    // [16 9 - 16 10]
    IL_0013: ret          

} // end of method Program::SetPropertyValue

将这段 IL 代码抄下来。怎么抄呢?看下面我抄的代码,你应该能够很容易看出里面一一对应的关系。

public static Action<object, object> CreatePropertySetter(PropertyInfo propertyInfo)
{
    var declaringType = propertyInfo.DeclaringType;
    var propertyType = propertyInfo.PropertyType;

    // 创建一个动态方法,参数依次为方法名、返回值类型、参数类型。
    // 对应着 IL 中的
    // .method private hidebysig static void
    //     SetPropertyValue(
    //     ) cil managed
    var method = new DynamicMethod("<set_Property>", typeof(void), new[] {typeof(object), typeof(object)});
    var il = method.GetILGenerator();

    // 定义形参。注意参数位置从 1 开始——即使现在在写静态方法。
    // 对应着 IL 中的
    //     object this,
    //     object 'value'
    method.DefineParameter(1, ParameterAttributes.None, "this");
    method.DefineParameter(2, ParameterAttributes.None, "value");

    // 用 Emit 生成 IL 代码。
    // 对应着 IL 中的各种操作符。
    il.Emit(OpCodes.Nop);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Castclass, declaringType);
    il.Emit(OpCodes.Ldarg_1);
    // 注意:下一句代码会在文章后面被修改。
    il.Emit(OpCodes.Castclass, propertyType);
    il.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
    il.Emit(OpCodes.Nop);
    il.Emit(OpCodes.Ret);

    // 为生成的动态方法创建调用委托,返回返回这个委托。
    return (Action<object, object>) method.CreateDelegate(typeof(Action<object, object>));
}

现在我们用下面新的代码替换之前写在 Main 中直接赋值的代码:

static void Main(string[] args)
{
    // 测试代码。
    var instance = new TempClass();
    var propertyInfo = typeof(TempClass).GetProperties().First();
    // 调用 Emit 核心代码。
    var setValue = QuickEmit.CreatePropertySetter(propertyInfo);
    // 测试生成的核心代码能否正常工作。
    setValue(instance, "test");
}

直接运行,在 setValue 之后我们查看 instanceTempProperty 属性的值,可以发现已经成功修改了。大功告成

快速调试和修改 Emit

才没有大功告成呢

试试把 TempProperty 的类型改为 int。把测试代码中传入的 "test" 字符串换成数字 5。运行看看:

VerificationException
▲ 为什么会崩溃?!

崩溃提示是“操作可能造成运行时的不稳定”。是什么造成了运行时的不稳定呢?难道是我们写的 IL 不对?

现在开始利用 dotPeek 进行 IL 的调试

我们编写另外一个方法,用于将我们的生成的 IL 代码输出到 dll 文件。

public static void OutputPropertySetter(PropertyInfo propertyInfo)
{
    var declaringType = propertyInfo.DeclaringType;
    var propertyType = propertyInfo.PropertyType;

    // 准备好要生成的程序集的信息。
    var assemblyName = new AssemblyName("Temp");
    var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);
    var module = assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll");
    var type = module.DefineType("Temp", TypeAttributes.Public);
    var method = type.DefineMethod("<set_Property>",
        MethodAttributes.Static - MethodAttributes.Public, CallingConventions.Standard,
        typeof(void), new[] { typeof(object), typeof(object) });
    var il = method.GetILGenerator();

    // 跟之前一样生成 IL 代码。
    method.DefineParameter(1, ParameterAttributes.None, "this");
    method.DefineParameter(2, ParameterAttributes.None, "value");

    il.Emit(OpCodes.Nop);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Castclass, declaringType);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Castclass, propertyType);
    il.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
    il.Emit(OpCodes.Nop);
    il.Emit(OpCodes.Ret);

    // 将 IL 代码输出到程序的同级目录下。
    type.CreateType();
    assembly.Save($"{assemblyName.Name}.dll");
}

同样的,作为对照,我们在我们的测试程序中也修改那个参考代码。

static void SetPropertyValue(object @this, object value)
{
    // 注意!注意!string 已经换成了 int。
    ((TempClass) @this).TempProperty = (int) value;
}

重新生成可以得到一个 exe,调用新写的 OutputPropertySetter 可以得到 Temp.dll。于是我们的输出目录下现在存在两个程序集:

两个输出程序集

将他们都拖进 dotPeek 中,然后在顶部菜单 Windows->IL Viewer 中打开 IL 显示窗格。

比较生成的 IL

发现什么了吗?是的!对于结构体,用的是拆箱!!!而不是强制类型转换。

知道有了拆箱,于是就能知道应该怎样改了,生成 IL 的代码中 Castclass 部分应该根据条件进行判断:

var castingCode = propertyInfo.PropertyType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass;
il.Emit(castingCode, propertyType);

现在运行,即可正常通过。如果你希望拥有完整的代码,可以自行将以上两句替换掉此前注释说明了 注意:下一句代码会在文章后面被修改。 的地方。

更进一步

  • 如果要 Emit 的代码中存在 if-else 这样的非顺序结构怎么办?阅读 使用 Emit 生成 IL 代码 - 吕毅 可以了解做法。
  • 我们可以用 intdouble 类型的属性赋值,但在本例代码中却不可行,如何解决这种隐式转换的问题?

如果你尝试编写了 Emit 的代码,那么上面的问题应该难不倒你。

总结

  1. 通过 Emit,我们能够在运行时动态生成 IL 代码,以解决反射动态调用方法造成的大量性能损失。
  2. 通过 ReSharper 插件,我们可以实时查看生成的 IL 代码。
  3. 我们可以将 Emit 生成的代码输出到程序集文件。
  4. 通过 dotPeek,我们可以查看程序集中类型和方法的 IL 代码。

参考资料

  • 生成方法签名与元数据
    • ParameterBuilder Class (System.Reflection.Emit)
    • MethodBuilder.DefineParameter Method (Int32, ParameterAttributes, String) (System.Reflection.Emit)
    • Defining a Parameter with Reflection Emit
    • c# - How to set “.maxstack” with ILGenerator - Stack Overflow
  • 生成方法体
    • ILGenerator.DefineLabel Method (System.Reflection.Emit)
    • ILGenerator.MarkLabel Method (Label) (System.Reflection.Emit)
    • c# - Emit local variable and assign a value to it - Stack Overflow
    • C# reflection: If … else? - Stack Overflow
    • ILGenerator.Emit Method
    • ILGenerator.Emit Method (System.Reflection.Emit)
    • ILGenerator.Emit Method (OpCode, String) (System.Reflection.Emit)
    • ILGenerator.Emit Method (OpCode, MethodInfo) (System.Reflection.Emit)
    • ILGenerator.EmitCall Method (OpCode, MethodInfo, Type[]) (System.Reflection.Emit)
    • .net - Call and Callvirt - Stack Overflow
  • IL 操作
    • OpCodes.Ldarg_0 Field (System.Reflection.Emit)
    • OpCodes.Brfalse_S Field (System.Reflection.Emit)
  • 输出程序集
    • c# - Is there a way to view the generated IL code of a DynamicMethod (in Sigil)? - Stack Overflow
    • c# - Can I use Reflection.Emit for generating code and save generated codes in .cs files or I could use CodeDom? - Stack Overflow
    • AssemblyBuilder.Save Method (String) (System.Reflection.Emit)
  • 运行时错误
    • c# - Reflection.emit System.InvalidProgramException: Common Language Runtime detected an invalid program - Stack Overflow
    • “Operation could destabilize the runtime.” when using IL to create a DynamicMethod · Issue #14 · jbevain/mono.reflection
    • c# - Emit Operation could destabilize the runtime for incrementing field - Stack Overflow
    • .NET 4.5 : Operation could destabilize the runtime (yikes!) - ElegantCode
    • c# - Operation could destabilize the runtime? - Stack Overflow
  • 其他
    • Generating and Compiling Source Code from a CodeDOM Graph - Microsoft Docs

相关文章:

  • 自动将 NuGet 包的引用方式从 packages.config 升级为 PackageReference
  • 冷算法:自动生成代码标识符(类名、方法名、变量名)
  • WPF/UWP 的 Grid 布局竟然有 Bug,还不止一个!了解 Grid 中那些未定义的布局规则
  • Git 更安全的强制推送,--force-with-lease
  • 项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦)
  • 理解 C# 项目 csproj 文件格式的本质和编译流程
  • 如何创建一个基于命令行工具的跨平台的 NuGet 工具包
  • 如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包
  • 如何最快速地将旧的 NuGet 包 (2.x, packages.config) 升级成新的 NuGet 包 (4.x, PackageReference)
  • 每次都要重新编译?太慢!让跨平台的 MSBuild/dotnet build 的 Target 支持差量编译
  • C# 中那些可以被重载的操作符,以及使用它们的那些丧心病狂的语法糖
  • 神器如 dnSpy,无需源码也能修改 .NET 程序
  • Roslyn 入门:使用 .NET Core 版本的 Roslyn 编译并执行跨平台的静态的源码
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • 微软 Windows 系统检测网络连通性(用于显示感叹号)竟然是通过访问一个特殊网址来实现的
  • [ JavaScript ] 数据结构与算法 —— 链表
  • 【Linux系统编程】快速查找errno错误码信息
  • Golang-长连接-状态推送
  • httpie使用详解
  • Javascript编码规范
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • JavaScript新鲜事·第5期
  • Laravel Mix运行时关于es2015报错解决方案
  • nodejs:开发并发布一个nodejs包
  • Python十分钟制作属于你自己的个性logo
  • scala基础语法(二)
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • Selenium实战教程系列(二)---元素定位
  • Spring Cloud(3) - 服务治理: Spring Cloud Eureka
  • underscore源码剖析之整体架构
  • v-if和v-for连用出现的问题
  • win10下安装mysql5.7
  • 大主子表关联的性能优化方法
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 为视图添加丝滑的水波纹
  • 阿里云重庆大学大数据训练营落地分享
  • ​草莓熊python turtle绘图代码(玫瑰花版)附源代码
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (论文阅读22/100)Learning a Deep Compact Image Representation for Visual Tracking
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • (转)淘淘商城系列——使用Spring来管理Redis单机版和集群版
  • (转)原始图像数据和PDF中的图像数据
  • (轉貼) UML中文FAQ (OO) (UML)
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .NET BackgroundWorker
  • .NET Framework 4.6.2改进了WPF和安全性
  • .net 使用ajax控件后如何调用前端脚本
  • .NET 中创建支持集合初始化器的类型
  • .NET精简框架的“无法找到资源程序集”异常释疑
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • /etc/sudoers (root权限管理)
  • /usr/bin/env: node: No such file or directory
  • @media screen 针对不同移动设备