使用元数据 API 和标记

更新:2007 年 11 月

元数据 API 可以从 C++ 调用。元数据 API 的用法部分取决于使用它们的客户端的类型。大多数元数据 API 客户端可以分为两类:

  • 编译器(例如 Visual C++ 2005 中的编译器),它们生成临时的 .obj 文件,然后在单独的链接器阶段将各个编译单元合并到单个目标可移植可执行 (PE) 文件中。

  • 应用程序快速开发 (RAD) 工具,在生成之前它们在工具环境中管理所有代码和数据结构;在生成时,它们将在单个步骤中生成并发出一个 PE 文件。

其他客户端对元数据 API 的使用方式可能介于这两种方式之间。某些工具可能允许元数据引擎执行优化,但可能并不需要标记重新映射信息。它们也可能仅需要部分标记类型的重新映射信息,但对于其他标记类型则不需要。实际上,即使发出了 .obj 文件,编译器也可能不执行优化。

“编译和链接”方式

在“编译和链接”方式的交互中,编译器前端使用 IMetaDataDispenserEx API 为内存中的元数据建立一个范围,然后使用 IMetaDataEmit API 声明类型和成员,并将它们用于元数据标记概述中描述的元数据抽象。但是,该前端将无法提供方法实现信息(例如,实现是托管的还是非托管的,是 MSIL 还是本机代码)或相对虚拟地址 (RVA) 信息,因为该信息在编译时是无法确定的。在编译完代码并将其发出到 PE 文件中时,将需要由后端(即链接器)来提供此信息。

此处的难点在于,后端工具需要能够获取有关元数据二进制文件的目标保存大小的信息,以便为其在 PE 文件中留出相应的空间。但是,如果该工具未识别方法 RVA 和模块级静态数据成员 RVA 并将它们发出到元数据中,便无法将元数据二进制文件保存到该文件中。为正确计算该目标保存大小,元数据引擎必须先执行任何预保存优化,因为在理想情况下,这些优化可使目标二进制文件变小。优化可能包括:对数据结构进行排序,以提高搜索速度;或当引用指向当前范围内声明的类型或成员时通过去除(早期绑定)mdTypeRefmdMemberRef 标记来进行优化。这些类型的优化可能导致重新映射元数据标记,而该工具必须能够重用这些标记才能发出实现和 RVA 信息。这样,该工具和元数据引擎必须协同工作来跟踪标记重新映射。

因此,在编译期间为保留元数据而执行的调用顺序如下:

  1. IMetaDataEmit::SetHandler,用于提供 IUnknown 接口,元数据引擎可以使用此接口来查询 IID_IMapToken(用于通知客户端有关标记重新映射的信息)。在创建了元数据范围后,可以在任何时间调用 SetHandler,但此调用一定要在调用 IMetaDataEmit::GetSaveSize 之前进行。

  2. IMetaDataEmit::GetSaveSize,用于获取元数据二进制文件的保存大小。GetSaveSize 使用 IMetaDataEmit::SetHandler 中提供的 IMapToken 接口来通知客户端有关任何标记重新映射的信息。如果未使用 SetHandler 来提供 IMapToken 接口,将不执行任何优化。这样,正在发出临时 .obj 文件的编译器便可以跳过不必要的优化(可能要在链接和合并阶段之后重新进行)。

  3. IMetaDataEmit::Save,在根据需要使用 IMetaDataEmit::SetRVA 和其他 IMetaDataEmit 方法发出最终的实现元数据后,此方法用于保留元数据二进制文件。

下一级别的难点来自链接器阶段,在此阶段,多个编译单元将合并到一个集成的 PE 文件中。在这种情况下,不仅需要合并元数据范围,而且 RVA 也将随新 PE 文件的发出而再次更改。在合并阶段中,每当调用 IMetaDataEmit::Merge 方法时,它都会作用于单个导入和单个发出范围:将元数据标记从导入范围重新映射到发出范围。另外,合并进程还可能遇到持续性错误,在此情况下,它必须能够将这些错误发送给客户端。当合并完成后,发出最终的 PE 文件还涉及调用 IMetaDataEmit::GetSaveSize 以及另一轮标记重新映射。

链接器为发出和保留元数据而执行的调用顺序如下:

  1. IMetaDataEmit::SetHandler,用于提供 IUnknown 接口,元数据引擎不仅可以使用此接口查询 IID_IMapToken(如前面的操作),还可以查询 IID_IMetaDataError。后一个接口用于通知客户端有关任何因合并而产生的持续性错误的信息。

  2. IMetaDataEmit::Merge,用于将指定的元数据范围合并到当前发出范围中。Merge 使用 IMapToken 接口通知客户端有关标记重新映射的信息,并使用 IMetaDataError 通知客户端有关持续性错误的信息。

  3. IMetaDataEmit::GetSaveSize,用于获取元数据二进制文件的目标保存大小。GetSaveSize 使用 IMetaDataEmit::SetHandler 中提供的 IMapToken 接口来通知客户端有关任何标记重新映射的信息。对于在 Merge 中出现并在执行格式优化后在 GetSaveSize 中再次出现的标记重新映射,必须准备相应的工具对其进行处理。关于标记的最后一个通知将代表该工具应依赖的最终映射。

  4. IMetaDataEmit::Save,在根据需要使用 IMetaDataEmit::SetRVA 和其他 IMetaDataEmit 方法发出最终的实现元数据后,此方法用于保留元数据二进制文件。

RAD 工具方式

与“编译和链接”方式的交互相同,RAD 工具也使用 IMetaDataDispenserEx 接口为内存中的元数据建立一个范围,然后使用 IMetaDataEmit 接口声明类型和成员,以将它们用于元数据标记概述中所述的元数据抽象。

与“编译和链接”方式不同的是,RAD 工具通常将在单个步骤中发出 PE 文件。它可能在单个传递中发出声明和实现信息,并可能始终不需要调用 IMetaDataEmit::Merge。因此,如果 RAD 工具需要处理复杂的标记重新映射,则唯一理由可能是为了利用当前由 IMetaDataEmit::GetSaveSize 执行的预保存优化。

通常情况下,如果一个工具可以发出完全优化的元数据,则无需元数据引擎也能发出经过合理优化的文件。但是,元数据引擎和文件格式的未来实现可能会使某些优化策略过时,因此存在一套清晰的规则来说明如何发出优化的元数据。

在发出元数据声明和实现信息后,执行的调用顺序如下:

  1. IMetaDataEmit::SetRVA 及其他 IMetaDataEmit 方法(根据需要),用于发出最终的实现元数据。

  2. IMetaDataEmit::Save,用于保留元数据二进制文件。

请参见

其他资源

元数据概述