欢迎光临散文网 会员登陆 & 注册

C# 反射与特性:EMIT 构建代码

2020-07-13 10:45 作者:微软MVP-Eleven  | 我要投稿


本期我们将学习 .NET Core 中,关于动态构建代码的知识。其中表达式树已经在另一个系列写了,所以本系列主要是讲述 反射,Emit ,AOP 等内容。

如果现在总结一下,反射,与哪些数据结构相关?

我们可以从 AttributeTargets 枚举中窥见:

分别是程序集、模块、类、结构体、枚举、构造函数、方法、属性、字段、事件、接口、参数、委托、返回值。

以往的文章中,已经对这些进行了很详细的讲解,我们可以中反射中获得各种各样的信息。当然,我们也可以通过动态代码,生成以上数据结构。

动态代码的其中一种方式是表达式树,我们还可以使用 Emit 技术、Roslyn 技术来编写;相关的框架有 Natasha、CS-Script 等。



01PART构建代码


首先我们引入一个命名空间:

using System.Reflection.Emit;

Emit 命名空间中里面有很多用于构建动态代码的类型,例如 AssemblyBuilder,这个类型用于构建程序集。类推,构建其它数据结构例如方法属性,则有 MethodBuilderPropertyBuilder 。

程序集(Assembly)


AssemblyBuilder 类型定义并表示动态程序集,它是一个密封类,其定义如下:

public sealed class AssemblyBuilder : Assembly

AssemblyBuilderAccess 定义动态程序集的访问模式,在 .NET Core 中,只有两个枚举:


另外,程序集的构建方式(API)也做了变更,如果你百度看到文章AppDomain.CurrentDomain.DefineDynamicAssembly,那么你可以关闭创建了,说明里面的很多代码根本无法在 .NET Core 下跑。

好了,不再赘述,我们来看看创建一个程序集的代码:

AssemblyName assemblyName = new AssemblyName("MyTest"); AssemblyBuilder
assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.Run);

构建程序集,分为两部分:

  • AssemblyName 完整描述程序集的唯一标识。

  • AssemblyBuilder 构建程序集

一个完整的程序集,有很多信息的,版本、作者、构建时间、Token 等,这些可以使用

AssemblyName 来设置。

一般一个程序集需要包含以下内容:

  • 简单名称。

  • 版本号。

  • 加密密钥对。

  • 支持的区域性。


你可以参考以下示例:

最终程序集的 AssemblyName 显示名称是以下格式的字符串:

Name <,culture cultureinfo=""> <,Version = Major.Minor.Build.Revi
sion> <, strongname=""> <,publickeytoken> '\0'

例如:

ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d
015c7d5a0b012

另外,创建程序集构建器使用 AssemblyBuilder.DefineDynamicAssembly() 而不是 new AssemblyBuilder() 。

模块(Module)


程序集和模块之间的区别可以参考

模块是程序集内代码的逻辑集合,每个模块可以使用不同的语言编写,大多数情况下,一个程序集包含一个模块。程序集包括了代码、版本信息、元数据等。

MSDN指出:“模块是没有 Assembly 清单的 Microsoft 中间语言(MSIL)文件。”。

这些就不再扯淡了。

创建完程序集后,我们继续来创建模块。

类型(Type)


目前步骤:

Assembly -> Module -> Type 或 Enum

ModuleBuilder 中有个 DefineType 方法用于创建 class 和 structDefineEnum方法用于创建 enum

这里我们分别说明。

创建类或结构体:

TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyCla
ss",TypeAttributes.Public);

定义的时候,注意名称是完整的路径名称,即命名空间+类型名称。

我们可以先通过反射,获取已经构建的代码信息:

结果:

接下来将创建一个枚举类型,并且生成枚举。

我们要创建一个这样的枚举:

namespace MyTest { public enum MyEnum { Top = 1, Bottom = 2, Lef
t = 4, Right = 8, All = 16 } }

使用 Emit 的创建过程如下:

EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEn
um", TypeAttributes.Public, typeof(int));

TypeAttributes 有很多枚举,这里只需要知道声明这个枚举类型为 公开的(Public);typeof(int) 是设置枚举数值基础类型。

然后 EnumBuilder 使用 DefineLiteral 方法来创建枚举。

代码如下:

我们可以使用反射将创建的枚举打印出来:

Main 方法中调用:

WriteEnum(enumBuilder.CreateTypeInfo());

接下来,类型创建成员,就复杂得多了。

DynamicMethod定义方法与添加IL


下面我们来为 类型创建一个方法,并通过 Emit 向程序集中动态添加 IL。这里并不是使用 MethodBuider,而是使用 DynamicMethod。

在开始之前,请自行安装反编译工具 dnSpy 或者其它工具,因为这里涉及到 IL 代码。

这里我们先忽略前面编写的代码,清空 Main 方法。

我们创建一个类型:

public class MyClass{}

这个类型什么都没有。

然后使用 Emit 动态创建一个 方法,并且附加到 MyClass 类型中:

运行后会打印字符串。

DynamicMethod 类型用于构建方法,定义并表示可以编译、执行和丢弃的一种动态方法。 丢弃的方法可用于垃圾回收。。

ILGenerator 是 IL 代码生成器。

EmitWriteLine 作用是打印字符串,

OpCodes.Ret 标记 结束方法的执行,

Invoke 将方法转为委托执行。

上面的示例比较简单,请认真记一下。

下面,我们要使用 Emit 生成一个这样的方法:

看起来很简单的代码,要用 IL 来写,就变得复杂了。

ILGenerator 正是使用 C# 代码的形式去写 IL,但是所有过程都必须按照 IL 的步骤去写。

其中最重要的,便是 OpCodes 枚举了,OpCodes 有几十个枚举,代表了 IL 的所有操作功能。

如果你点击上面的链接查看 OpCodes 的枚举,你可以看到,很多 功能码,这么多功能码是记不住的。我们现在刚开始学习 Emit,这样就会难上加难。

所以,我们要先下载能够查看 IL 代码的工具,方便我们探索和调整写法。

我们看看此方法生成的 IL 代码:

看不懂完全没关系,因为笔者也看不懂。

目前我们已经获得了上面两大部分的信息,接下来我们使用 DynamicMethod 来动态编写方法。

定义 Add 方法并获取 IL 生成工具:

DynamicMethod 用于定义一个方法;ILGenerator是 IL 生成器。当然也可以将此方法附加到一个类型中,完整代码示例如下:

实际以上代码与我们反编译出来的 IL 编写有所差异,具体俺也不知道为啥,在群里问了调试了,注释掉那么几行代码,才通过的。


C# 反射与特性:EMIT 构建代码的评论 (共 条)

分享到微博请遵守国家法律