使用 Reflection.Emit 进行动态程序集。第二部分(共两部分)- Reflection.Emit






4.24/5 (8投票s)
本文解释了如何使用 Reflection.Emit 类动态生成 .NET 程序集。
阅读第一部分 这里。
引言
MSDN 称
System.Reflection.Emit
命名空间包含允许编译器或工具发出元数据和 Microsoft 中间语言 (MSIL) 并选择性地在磁盘上生成 PE 文件的类。这些类的主要使用者是脚本引擎和编译器。
再次 借用 MSDN 的说法,Reflection.Emit 提供了以下功能:
- 使用 DynamicMethod 类在运行时定义轻量级全局方法,并使用委托执行它们。
- 在运行时定义程序集,然后运行它们和/或将它们保存到磁盘。
- 在运行时在新程序集中定义模块,然后运行它们和/或将它们保存到磁盘。
- 在运行时在模块中定义类型,创建这些类型的实例,并调用它们的方法。
- 为定义的模块定义符号信息,可供调试器和代码分析器等工具使用。
这些听起来是不是很酷,也很技术宅?我可能会在某一天写写所有这些功能,但现在我的目标是利用 `Reflection.Emit` 的程序集生成功能,生成一个与我们在本文第一部分使用 CodeDOM 和动态代码编译生成的程序集相似的程序集。如果您还没有阅读过,可以在 这里 找到它。
我们生成的程序集将与第一部分中生成的相同。但我们如何做到呢?使用 `Reflection.Emit` “发出”程序集也是一个多步骤的过程,就像 CodeDOM 一样。在本部分中,我们也将直接开始在名为 `MSILGenerator` 的类中使用 `Reflection.Emit` 实现 `GenerateAssembly` 方法。
Reflection.Emit 的结构
`Reflection.Emit` 命名空间包含一些称为“生成器”(builders)的类,用于生成方法、类型(或类)、构造函数、程序集等各种结构。这些生成器提供了一个 `ILGenerator`,可用于发出所需的指令码。我们怎么会知道要生成哪些指令码?我们又如何确保它们是正确的?
正如许多人推荐的那样,我也觉得最快的学习方法是编写简单的代码(例如,包含一个空类的程序集),构建它,然后使用反汇编器 ILDASM 来检查它。然后,我们可以尝试使用 `Reflection.Emit` 来生成类似的程序集。
由于本文的目标是生成与第一部分中使用 CodeDOM 生成的程序集相同的程序集,让我们通过 ILDASM 来查看一个生成的程序集。它看起来是这样的:

我们的方法是使用 `Reflection.Emit` 来按照此处所示的相同顺序生成相同的代码。A、B 和 C 部分尝试实现完全相同的目标。上面图像中显示的内容与我在这些部分中编写的代码以生成相同内容之间存在直接关联。
还有一点很重要,就是我们不需要生成每一行 MSIL。`Reflection.Emit` 为我们隐藏了许多复杂性。当我们下面生成一些代码时,会对此有更多说明。
任何可执行的 .NET 代码都需要应用程序域、程序集、模块、类型和方法的正确上下文。实际编写在方法边界内的代码是我们执行的最后一步。下面这张从 Dr. Dobb's 关于使用 Reflection.Emit 在运行时生成代码的文章 借来的图片,展示了 `Reflection.Emit` 提供的用于辅助代码生成过程的生成器类集。

A. 定义程序集
我们可以在当前应用程序域或新应用程序域中定义程序集。新的应用程序域将使我们能够控制动态程序集的加载和“卸载”,这在我们实际应用中可能需要这样做。在本例中,我使用的是当前应用程序域。
#region Define Assembly
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "Derived";
AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Save);
Type[] constructorParameters = new Type[] { typeof(string), typeof(GenerationMode) };
ConstructorInfo constructorInfo =
typeof(AssemblyLevelAttribute).GetConstructor(constructorParameters);
CustomAttributeBuilder assemblyLevelAttribute = new CustomAttributeBuilder(
constructorInfo,
new object[] { assemblyAuthorName, GenerationMode.IL });
assemblyBuilder.SetCustomAttribute(assemblyLevelAttribute);
#endregion
`Reflection.Emit` 提供了几个称为“生成器”的类,用于创建类、方法、构造函数、属性等不同的编程结构。
您可能已经猜到,我们需要的第一样东西是 `AssemblyBuilder` 类。`AssemblyBuilder` 允许您定义目标程序集。它需要一个 `AssemblyName`(这不是简单的 `string`,而是 `AssemblyName` 类型的对象)。`AssemblyBuilder` 构造函数还要求我们指定生成程序集的 `AccessLevel`(是只允许运行/保存/两者都允许/仅反射)。由于我们只对生成程序集感兴趣,而不是运行它,所以我选择了 `Save` 作为程序集的访问级别。
现在我们需要为我们定义的程序集应用程序集级别的属性。我们有一个名为 `CustomAttributeBuilder` 的类,正好用于此目的。要创建 `CustomAttributeBuilder` 的对象,我们需要定义自定义属性的类型的 `ConstructorInfo`。我们通过调用类型的 `GetConstructor` 方法来完成此操作。然后,此构造函数信息以及类型构造函数的参数值的对象数组会被馈送到自定义属性生成器。最后,我们使用程序集生成器的 `SetCustomAttribute` 方法将此程序集级别的自定义属性设置为我们的程序集。
B. 定义模块
#region Define module (code container)
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule
( "DerivedAssembly", "DerivedAssembly.dll");
#endregion
下一步是创建一个充当代码容器的模块。这是使用 `ModuleBuilder` 完成的。
C. 定义类型(类)
步骤
- 使用 `TypeBuilder` 来定义类型。这是一个重载方法(6 个重载),我们使用的是接受类型名称(类名)、属性和基类型的那个。
- 像在 A 节中一样,使用 `CustomAttributeBuilder` 定义类级别的属性。将自定义属性应用于类型生成器。
#region Create the derived class with the entity subscription attribute
//First, we'll build a type
TypeBuilder typeBuilder = moduleBuilder.DefineType("Derived",
TypeAttributes.Public | TypeAttributes.BeforeFieldInit,
typeof(Base.Base));
//Let us apply the required attribute
constructorParameters = new Type[] { typeof(string) };
constructorInfo = typeof(ClassLevelAttribute).GetConstructor(constructorParameters);
CustomAttributeBuilder classLevelAttributeBuilder =
new CustomAttributeBuilder( constructorInfo,
new object[] { classAuthorName });
typeBuilder.SetCustomAttribute(classLevelAttributeBuilder);
#endregion
D. 类构造函数
下一步是定义类构造函数。默认构造函数会自动提供,但学习如何创建构造函数(如果您需要带参数的构造函数,或者想修改默认构造函数的行为)很重要。
我们的目标 MSIL 代码如下所示:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void
[DynamicCodeGeneration.Base]DynamicCodeGeneration.Base.Base::.ctor()
IL_0006: ret
} // end of method Derived::.ctor
正如我之前提到的,我们不关心生成每一行。上面非粗体的行由 IL 生成器处理。我们只需要输出实际操作的指令码,其他的“管道”任务会透明地处理。
生成上述 MSIL 的步骤(粗体部分是我们控制/生成的)是:
- 使用 `ConstructorBuilder` 类,通过类型生成器的 `DefineConstructor` 方法在类型中定义构造函数。
- 现在我们准备开始“发出”程序集了。
- 从构造函数生成器中获取 IL 生成器,并发出上面显示的必需指令码。
#region Constructor
//Define the default constructor
//(not required actually as default constructor is automatically supplied)
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
System.Type.EmptyTypes);
ILGenerator generator = constructorBuilder.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Call,
typeBuilder.BaseType.GetConstructor(System.Type.EmptyTypes));
generator.Emit(OpCodes.Ret);
#endregion
E. 重写方法
现在我们需要在派生类中定义重写的方法。此方法的 MSIL 为:
.method public hidebysig virtual instance void
Method() cil managed
{
.custom instance void [DynamicCodeGeneration.CustomAttributes]
DynamicCodeGeneration.CustomAttributes.MethodLevelAttribute::.ctor(valuetype
[DynamicCodeGeneration.CustomAttributes]
DynamicCodeGeneration.CustomAttributes.ComplexityLevel,
string) = ( 01 00 02 00 00 00 07 54 72 69 6E 69 74 79 00 00 ) // .......Trinity..
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [DynamicCodeGeneration.Base]DynamicCodeGeneration.Base.Base::Method()
IL_0006: ret
} // end of method Derived::Method
生成上述内容的步骤如下:
- 使用 `DefineMethod` 创建 `MethodBuilder` 对象。
- 以与 A 节和 C 节类似的方式应用方法级别的属性。
- 获取方法的 `ILGenerator` 并发出必需的指令码。
#region Method
// Now, let's build a method and add a custom attribute to it.
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Method",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual,
null,
new Type[] { });
//Apply the event attribute to method
constructorParameters = new Type[] { typeof(ComplexityLevel), typeof(string) };
constructorInfo = typeof(MethodLevelAttribute).GetConstructor(constructorParameters);
CustomAttributeBuilder methodAttributeBuilder = new CustomAttributeBuilder(
constructorInfo,
new object[] { ComplexityLevel.SuperComplex, methodAuthorName });
methodBuilder.SetCustomAttribute(methodAttributeBuilder);
generator = methodBuilder.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Call, typeof(Base.Base).GetMethod("Method"));
generator.Emit(OpCodes.Ret);
#endregion
F. 总结
发出所有必需的指令码后,我们需要关闭该类型。此操作将类“盖棺定论”,无法再向该类型发出任何指令码。此时,我们可以选择保存程序集。
typeBuilder.CreateType(); //Close the type
assemblyBuilder.Save("Derived.dll");
return "Derived.dll";
结论
至此,本两部分文章结束。我们了解了如何利用 CodeDOM 和 `Reflection.Emit` 技术动态生成程序集。我们看到了如何在程序集、类和方法级别应用自定义属性。这为我们提供了一种非常强大的机制,我们可以在工作中以多种不同的方式利用它。动态代码生成有许多潜在的用途,而在 .NET 中,它不仅容易,而且很有趣!
免责声明
在撰写这些文章时,我广泛借鉴了网上提供的各种资源。虽然我已尽最大努力不遗漏参考列表中的任何文章,但我可能无意中遗漏了一些引用。请将此类遗漏通知我,我很乐意将其包含在我的文章参考文献中。
参考文献
- Chris Sells 和 Shawn Van Ness 在 Dr. Dobb's 网站上的 《使用 Reflection.Emit 在运行时生成代码》
- MSDN
- Steve Eichert 关于 动态生成程序集 的 观点
延伸阅读
- Oman van Kloeten 的 CodeDOM 模式
- 代码生成 网络 上的大量文章
- 使用 `Reflection.Emit` 创建动态类型简介
- EmitHelper
关于 Proteans 软件解决方案
Proteans 是一家专注于软件产品开发和基于 Microsoft 技术平台的业务应用程序开发的 외包公司。Proteans 与独立软件供应商 (ISV)、系统集成商和企业 IT 团队合作开发软件产品。我们的技术专长和在软件产品开发——设计、构建和发布世界一流的、健壮且可扩展的软件产品方面的丰富经验,帮助我们为客户缩短上市时间、降低成本、减少业务风险并提高整体业务成果。Proteans 在使用 Microsoft .NET 技术进行开发方面拥有专业知识。
历史
- 2007 年 5 月 6 日:首次发布