如何读取winmd(WinRT元数据文件)?

13

WinMD是二进制元数据文件,包含有关本地WinRT dll中可用命名空间、类型、类、方法和参数的所有信息。

根据Windows Runtime设计

使用API元数据(.winmd文件)公开Windows Runtime。这与.NET框架(Ecma-335)使用的格式相同。底层的二进制合同使您可以直接访问所选开发语言中的Windows Runtime API。

每个.winmd文件都公开一个或多个命名空间。这些命名空间按提供的功能进行分组。命名空间包含类型,例如类、结构和枚举。

那么,如何访问它呢?

Winmd是COM

在底层,WinRT仍然是COM。而在WinRT中,Winmd(Windows元数据)则是从COM继承而来的旧TLB(类型库)文件的现代版本。

| COM                        | WinRT                          |
|----------------------------|--------------------------------|
| CoInitialize               | RoInitialize                   |
| CoCreateInstance(ProgID)¹  | RoActivateInstance(ClassName)  |
| *.tlb                      | *.winmd                        |
| compiled from idl          | compiled from idl              |
| HKCR\Classes\[ProgID]      | HKLM\Software\Microsoft\WindowsRuntime\ActivatableClassId\[ClassName] |
| Code stored in native dll  | Code stored in native dll      |
| DllGetClassObject          | DllGetClassObject              |
| Is native code             | Is native code                 |
| IUnknown                   | IUnknown (and IInspectible)    |
| stdcall calling convention | stdcall calling convention     |
| Everything returns HRESULT | Everything returns HRESULT     |
| LoadTypeLib(*.tlb)         | ???(*.winmd)                   |

从COM tlb文件中读取元数据

如果给定一个COM tlb文件(例如 stdole.tlb),您可以使用各种Windows函数来解析tlb并从中获取信息。

调用LoadTypeLib可获取ITypeLib接口:

ITypeLib tlb = LoadTypeLib("c:\Windows\system32\stdole2.tlb");

然后您可以开始迭代类型库中的所有内容。

for (int i = 0 to tlb.GetTypeInfoCount-1)
{
   ITypeInfo typeInfo = tlb.GetTypeInfo(i);
   TYPEATTR typeAttr = typeInfo.GetTypeAttr();

   case typeAttr.typeKind of
   TKIND_ENUM: LoadEnum(typeINfo, typeAttr);
   TKIND_DISPATCH,
   TKIND_INTERFACE: LoadInterface(typeInfo, typeAttr);
   TKIND_COCLASS: LoadCoClass(typeInfo, typeAttr);
   else
      //Unknown
   end;
   typeInfo.ReleaseTypeAttr(typeAttr);
}

在WinRT世界中,我们如何处理*.winmd文件?

根据Larry Osterman的说法:

从idl文件生成winmd文件。 winmd文件是类型的规范定义。然后将其传递给语言投影。 语言投影读取winmd文件,它们知道如何获取该winmd文件的内容-这是一个二进制文件-并针对该语言生成适当的语言结构。

它们都读取那个winmd文件。 它恰好是一个ECMA-335仅包含元数据的程序集。 这是包装文件格式的技术详细信息。

生成winmd的好处之一是因为它是规则的,所以现在我们可以构建工具来对winmd文件中的方法和类型进行排序、收集、合并。

从winmd加载元数据

我尝试使用RoGetMetaDataFile来加载WinMD。但RoGetMetaDataFile不是用于直接处理winmd文件的。它的目的是让您发现有关已知存在的类型和其名称的信息。

如果您传递了winmd文件名,则会调用RoGetMetadataFile失败:

HSTRING name = CreateWindowsString("C:\Windows\System32\WinMetadata\Windows.Globalization.winmd");
IMetaDataImport2 mdImport;
mdTypeDef mdType;

HRESULT hr = RoGetMetadataFile(name, null, null, out mdImport, out mdType);


0x80073D54
The process has no package identity

这对应于AppModel错误代码:

#define APPMODEL_ERROR_NO_PACKAGE        15700L

但是,如果你传递一个类,RoGetMetadataFile会成功:

RoGetMetadataFile("Windows.Globalization.Calendar", ...);

元数据分配器

有人建议使用MetaDataGetDispenser来创建一个IMetaDataDispenser

IMetaDataDispenser dispenser;
MetaDataGetDispenser(CLSID_CorMetaDataDispenser, IMetaDataDispenser, out dispenser);

可以使用 OpenScope 方法来打开一个 winmd 文件:

打开一个现有的磁盘文件,并将其元数据映射到内存中。
该文件必须包含公共语言运行时(CLR)元数据。

其中第一个参数 (Scope) 是 "要打开的文件的名称"

因此我们尝试:

IUnknown unk;
dispenser.OpenScope(name, ofRead, IID_?????, out unk);

除了我不知道应该要求哪个接口;文档也没有说明。它有这样的陈述:

内存中的元数据副本可以使用一个“导入”接口中的方法进行查询,或者可以使用一个“发射”接口中的方法进行添加。

强调词汇“导入”和“发射”的作者可能是在提供线索-而不是直接回答问题。

额外交谈

  • 我不知道winmd中的命名空间或类型(这就是我们要解决的问题)
  • 对于WinRT,我没有在CLR中运行托管代码; 这是用于本地代码

我们可以为这个问题设定假想动机,即我们将创建一种尚未存在投影的语言(例如ada,bpl,b,c)。另一个假想动机是允许IDE能够显示winmd文件的元数据内容。

另外,请记住WinRT与.NET没有任何关系。

  • 它不是托管代码。
  • 它不存在于程序集中。
  • 它不在.NET运行时内运行。
  • 但是由于.NET已经为您提供了一种与COM互操作的方式(考虑到WinRT是COM)
  • 您能够从托管代码中调用WinRT类

许多人似乎认为WinRT是.NET的另一个名称。 WinRT不使用、需要或在.NET、C#、.NET框架或.NET运行时中运行。

  • WinRT适用于本地代码
  • .NET Framework Class Library适用于托管代码

WinRT是本地代码的类库。 .NET人员已经有了自己的类库。

额外问题

本机mscore中有哪些函数可以处理ECMA-335二进制文件的元数据?

额外阅读


2
xlang 项目包括一个 winmd 阅读器。示例程序将 winmd 转换回 MIDL3实现细节。听起来 xlang 项目正在做你感兴趣的事情:为新语言添加投影。(我认为他们现在正在开发 Python。) - Raymond Chen
2
或者您可以使用MetadataGetDispenser打开winmd文件并请求IMetaDataImport,然后调用各种EnumXxx方法来进行嗅探。 - Raymond Chen
1
这不是一个直接的答案,但你至少可以使用 Visual Studio 自带的 ILDasm 查看 .winmd 文件的内容。 - Fredrik Orderud
1
ILSpy 也可以查看它们。.NET Reflector 可能也可以,但我不能确定它是否是付费商业软件。 - Ian Boyd
1
Raymond提到的示例程序现在已经放在这里 - Laurie Stearn
3个回答

8

一个问题是 IMetadataDispsenser.OpenScope 有两套文档:

而 Windows Runtime 文档没有提供任何文档:

riid

所需元数据接口的 IID,调用方将使用该接口导入(读取)或发出(写入)元数据。

.NET Framework 版本提供了文档:

riid

[in] 要返回的所需元数据接口的 IID;调用方将使用此接口来导入(读取)或发出(写入)元数据。

riid 的值必须指定为“导入”或“发出”接口之一。有效的值包括:

  • IID_IMetaDataImport
  • IID_IMetaDataImport2
  • IID_IMetaDataAssemblyImport
  • IID_IMetaDataEmit
  • IID_IMetaDataEmit2
  • IID_IMetaDataAssemblyEmit

现在我们可以开始将所有内容整合在一起。


  1. 创建元数据分配器:

     IMetadataDispsener dispener;
     MetaDataGetDispenser(CLSID_CorMetaDataDispenser, IMetaDataDispenser, out dispenser);
    
  2. 使用OpenScope指定要读取的.winmd文件。我们请求IMetadataImport接口,因为我们想从winmd中导入数据(而不是将其导出.winmd):

     //打开我们要转储的winmd文件
     String filename = "C:\Windows\System32\WinMetadata\Windows.Globalization.winmd";
    
     IMetaDataImport reader; //IMetadataImport2支持泛型
     dispenser.OpenScope(filename, ofRead, IMetaDataImport, out reader); //"Import"用于读取元数据。"Emit"用于写入元数据。
    
  3. 一旦您拥有元数据导入程序,就可以开始枚举元数据文件中的所有类型:

     Pointer enum = null;
     mdTypeDef typeID;
     Int32 nRead;
     while (reader.EnumTypeDefs(enum, out typeID, 1, out nRead) = S_OK)
     {
        ProcessToken(reader, typeID);
     }
     reader.CloseEnum(enum);
    
  4. 现在,对于winmd中的每个typeID,您可以获取各种属性:

     void ProcessToken(IMetaDataImport reader, mdTypeDef typeID)
     {
        //获取标记的三个有趣属性:
        String      typeName;       //例如“Windows.Globalization.NumberFormatting.DecimalFormatter”
        UInt32      ancestorTypeID; //此类型祖先的标记(例如Object、Interface、System.ValueType、System.Enum)
        CorTypeAttr flags;          //关于类型的各种标志(例如public、private、是接口)
    
        GetTypeInfo(reader, typeID, out typeName, out ancestorTypeID, out flags);
     }
    

 

现在我们到了需要一些巧妙技巧来获取有关类型信息的点:

  • 如果类型在 .winmd 文件本身中定义:使用 GetTypeDefProps
  • 如果类型是对存在于另一个 winmd 中的类型的 "引用":使用 GetTypeRefProps

区分它们的唯一方法是尝试读取类型属性,假定它是一个类型 定义,并检查返回值:

  • 如果返回 S_OK,则它是一种类型的 引用
  • 如果返回 S_FALSE,则它是一种类型的 定义

 

  • 获取类型的属性,包括:

    • typeName:例如“Windows.Globalization.NumberFormatting.DecimalFormatter”
    • ancestorTypeID:例如0x10000004
    • flags:例如0x00004101
  •  

        void GetTypeInfo(IMetaDataImport reader, mdTypeDef typeID, 
              out String typeName, DWORD ancestorTypeID, CorTypeAttr flags)
        {
           DWORD nRead;
           DWORD tdFlags;
           DWORD baseClassToken;
    
           hr = reader.GetTypeDefProps(typeID, null, 0, out nRead, out tdFlags, out baseClassToken);
           if (hr == S_OK)
           {
              //Allocate buffer for name
              SetLength(typeName, nRead);
              reader.GetTypeDefProps(typeID, typeName, Length(typeName),
                    out nRead, out flags, out ancestorTypeID);
              return;
           }
    
           //We couldn't find it as a type **definition**. 
           //Try again as a type **reference**.
           hr = reader.GetTypeRefProps(typeID, null, 0, out nRead, out tdFlags, out baseClassToken);
           if (hr == S_OK)
           {
              //Allocate buffer for name
              SetLength(typeName, nRead);
              reader.GetTypeRefProps(typeID, typeName, Length(typeName),
                    out nRead, out flags, out ancestorTypeID);
              return;
           }       
        }
    

    如果你试图解密类型,还有一些其他有趣的陷阱。在 Windows Runtime 中,一切都是基本的:

    • 一个接口
    • 或者一个类

    结构体和枚举也是类;但是它们是特定类的后代:

    • 接口
      • System.ValueType --> 结构体
      • System.Enum --> 枚举

    无价的帮助来自于:

    我相信这是唯一的文档,使用 Microsoft 的 API 从 EMCA-335 组装中读取元数据。


    关于GetTypeDefProps,上面说“如果返回S_OK,则为类型引用”,“如果返回S_FALSE,则为类型定义”,但是常识和示例代码似乎表明实际情况恰恰相反。 - Lexikos
    令牌值的高字节表示令牌类型(来源:https://bytepointer.com/resources/pietrek_dotnet_metadata_dll_hell.htm和SDK corhdr.h),因此您可以检查它以区分mdTypeDef和mdTypeRef值(无需按上面所示顺序尝试GetTypeDefProps和GetTypeRefProps)。虽然我认为API不会像上面暗示的那样将它们混合在一起;即您只从EnumTypeDefs获取mdTypeDef,而只从EnumTypeRefs获取mdTypeRef。 - Lexikos

    2

    我将从评论中提取一个答案,因为这对我很有帮助:

    如何在 GUI 中查看 WinMD 文件

    如果您想要查看 WinMD 文件(即在 GUI 中与其交互,直观地查看文件的组件),您可以使用 Visual Studio 中包含的 ildasm.exe 来查看它。

    您还可以使用 ILSpy(开源)和可能的 .NET Reflector(付费)。

    (来自 Fredrik OrderudIan Boyd

    0

    .winmd文件遵循ECMA-335标准,因此任何能够读取.NET程序集的代码都可以读取.winmd文件。

    我个人使用过的两个选项是Mono.CecilSystem.Reflection.Metadata。我个人发现Mono.Cecil更容易使用。


    1
    不幸的是,我认为这两个只能在CLR内运行。 - Ian Boyd
    你需要用C++来做吗? - Sunius

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接