.NET Core/.NET Framework 的 System.Reflection.Emit
命名空间为我们提供了动态生成 IL 代码的能力。利用这项能力,我们能够在运行时生成一段代码/一个方法/一个类/一个程序集。
本文将介绍使用 Emit 生成 IL 代码的方法,以及在此过程中可能遇到的各种问题。
用 Emit 生成 IL 代码时,很多我们写 C# 时不会注意到的问题现在都需要开始留意。
在阅读本文之前,希望统一一个平时可能不太留意的英文:
如果不了解它们之间的区别,请自行搜索。
在 IL 中,方法名称可以使用比 C# 更多的字符,例如“<”和“>”,这也是 C# 编译闭包时喜欢使用的字符。目前我还没有找到 IL 中哪些字符可以作为标识符名称,但从混淆工具来看,是比 C# 多得多的。
如果你试图生成实例方法,那么实例本身 this
将成为第一个参数,不过并不需要额外将它定义到参数列表中。
当然,如果是静态方法,我们能够自己指定一个 this
参数,不过没有实际的意义。
var method = new DynamicMethod("<MethodName>", typeof(void), new[] { typeof(object), typeof(object) }); method.DefineParameter(1, ParameterAttributes.None, "this"); method.DefineParameter(2, ParameterAttributes.None, "value");
如果不声明形参,那么生成的 IL 代码的函数将无法被正常调用(提示可能造成运行时的不稳定)。
平时写 C# 的时候,可能一个方法里面没有定义任何一个局部变量,但 IL 可不一定这么认为。
例如:
int a = 0; if (value.GetType() == typeof(string)) { } else { }
实际上,在 IL 中,除了 Int32
类型的 a
之外,还会额外定义一个 bool
类型的局部变量 V_1
。在 value.GetType() == typeof(string)
执行完后,其值将存入 V_1
。
所以,如果需要编写 Emit 生成代码的代码,需要注意这些隐式产生的局部变量,它们需要和普通变量一样被初始化。
Emit 代码为:
// 这就声明了两个局部变量。 il.DeclareLocal(typeof(int)); il.DeclareLocal(typeof(bool));
如果代码中存在非线性结构,例如 if
- else
,那么 IL 就需要知道跳转的地址。那么如何能够知道跳转到哪个地址呢?
对 if
- else
来说, if
操作需要知道 else
的起始地址;对于 if
内部的结尾来说,需要知道整个 if
- else
结束之后的第一个操作的地址。
var startOfElse = il.DefineLabel(); var endOfWholeIfElse = il.DefineLabel(); il.Emit(OpCodes.Nop); // 其他生成代码。 // 如果 if 条件不满足,跳转到 startOfElse。 il.Emit(OpCodes.Brfalse_S, startOfElse); // 其他生成代码。 // 在 if 结束之后,跳转到 endOfWholeIfElse 地址。 il.Emit(OpCodes.Br_S, endOfWholeIfElse); // 其他生成代码。 // 假设这里到了 else 的开头了,于是将 startOfElse 进行标记。标记完紧跟着写 else 部分的代码。 il.MarkLabel(startOfElse); il.Emit(OpCodes.Nop); // 其他生成代码。 // 假设这里整个 if-else 结束了,于是将 endOfWholeIfElse 进行标记。 il.MarkLabel(endOfWholeIfElse);
本文会经常更新,请阅读原文: https://walterlv.github.io/post/generate-il-using-emit.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。