清单文件格式

清单文件的文件格式尽可能多地从 C++ 和 IDL 借用。 因此,采用普通的 C++ SDK 头文件并将其修改为清单文件相当容易。 分析程序完全支持 C 和 C++ 样式注释,以帮助组织和记录文件。

如果尝试添加清单文件或对现有文件进行更改,最好的方法是进行试验。 在调试器中发出 !logexts.logi!logexts.loge 命令时,Logger 将尝试分析清单文件。 如果遇到问题,将生成一条错误消息,可能指示错误。

清单文件由以下基本元素组成:模块标签、类别标签、函数声明、COM 接口定义和类型定义。 其他类型的元素也存在,但这些元素是最重要的。

模块标签

模块标签只是声明 DLL 导出随后声明的函数的内容。 例如,如果清单文件用于从 Comctl32.dll 记录一组函数,则会在声明任何函数原型之前包含以下模块标签:

module COMCTL32.DLL:

模块标签必须显示在清单文件中的任何函数声明之前。 清单文件可以包含任意数量的模块标签。

类别标签

与模块标签类似,类别标签标识所有后续函数和/或 COM 接口所属的“类别”。 例如,如果要创建Comctl32.dll清单文件,则可以将以下内容用作类别标签:

category CommonControls:

清单文件可以包含任意数量的类别标签。

函数声明

函数声明是实际提示 Logger 记录内容的内容。 它几乎与 C/C++ 头文件中的函数原型完全相同。 格式有一些值得注意的新增功能,以下示例可以最好地说明这一点:

HANDLE [gle] FindFirstFileA(
       LPCSTR lpFileName,
       [out] LPWIN32_FIND_DATAA lpFindFileData);

函数 FindFirstFileA 采用两个参数。 第一个是 lpFileName,它是一个完整路径, (通常使用通配符) 定义搜索文件的位置。 第二个是指向WIN32_FIND_DATAA结构的指针,该结构将用于包含搜索结果。 返回的 HANDLE 用于将来对 FindNextFileA 的调用。 如果 FindFirstFileA 返回INVALID_HANDLE_VALUE,则函数调用失败,可以通过调用 GetLastError 函数获取错误代码。

HANDLE 类型声明如下:

value DWORD HANDLE
{
#define NULL                       0 [fail]
#define INVALID_HANDLE_VALUE      -1 [fail]
};

如果此函数返回的值为 0 或 -1 (0xFFFFFFFF) ,则 Logger 将假定该函数失败,因为此类值在值声明中具有 [fail] 修饰符。 (请参阅本节后面的“值类型”部分。) 由于函数名称前面有 [gle] 修饰符,Logger 识别此函数使用 GetLastError 返回错误代码,因此它会捕获错误代码并将其记录到日志文件。

lpFindFileData 参数上的 [out] 修饰符通知记录器数据结构由函数填充,应在函数返回时记录数据结构。

COM 接口定义

COM 接口基本上是可由 COM 对象的客户端调用的函数向量。 清单格式大量借用 COM 中用于定义接口的接口定义语言 (IDL) 。

请考虑以下示例:

interface IDispatch : IUnknown
{
    HRESULT GetTypeInfoCount( UINT pctinfo  );
 
    HRESULT GetTypeInfo(
        UINT iTInfo,
        LCID lcid,
        LPVOID ppTInfo );
 
    HRESULT GetIDsOfNames(
        REFIID riid,
        LPOLECHAR* rgszNames,
        UINT cNames,
        LCID lcid,
        [out] DISPID* rgDispId );
 
    HRESULT Invoke( 
        DISPID  dispIdMember,      
        REFIID  riid,              
        LCID  lcid,                
        WORD  wFlags,              
        DISPPARAMS*  pDispParams,  
        VARIANT*  pVarResult,  
        EXCEPINFO*  pExcepInfo,  
        UINT*  puArgErr );
};

这会声明一个名为 IDispatch 的接口,该接口派生自 IUnknown。 它包含四个成员函数,这些函数在接口的大括号内按特定顺序声明。 记录器将通过替换接口 vtable 中的函数指针来截获和记录这些成员函数, (运行时使用的函数指针的实际二进制向量,) 替换为自己的函数指针。 有关 Logger 如何在分发接口时捕获接口的详细信息,请参阅本节后面的 COM_INTERFACE_PTR 类型部分。

类型定义

定义数据类型是清单文件开发中最重要的 (和最繁琐) 部分。 清单语言允许你为传入或从函数返回的数值定义用户可读的标签。

例如,Winerror.h 定义一个名为“WinError”的类型,该类型是大多数 Microsoft Win32 函数返回的错误值及其相应的用户可读标签的列表。 这允许 Logger 和 LogViewer 将非信息错误代码替换为有意义的文本。

还可以标记位掩码内的单个位,以允许 Logger 和 LogViewer 将 DWORD 位掩码分解为其组件。

清单支持 13 种基本类型。 下表中列出了这些字段。

类型 Length 显示示例

指针

4 个字节

0x001AF320

VOID

0 字节

BYTE

1 个字节

0x32

WORD

2 个字节

0x0A23

DWORD

4 个字节

-234323

BOOL

1 个字节

TRUE

LPSTR

长度字节加上任意数量的字符

“快速棕色狐狸”

LPWSTR

长度字节加上任意数量的 Unicode 字符

“跳过懒狗”

GUID

16 个字节

{0CF774D0-F077-11D1-B1BC-00C04F86C324}

COM_INTERFACE_PTR

4 个字节

0x0203404A

依赖于基类型

ERROR_TOO_MANY_OPEN_FILES

掩码

依赖于基类型

WS_MAXIMIZED |WS_ALWAYSONTOP

struct

取决于封装类型的大小

+ lpRect nLeft 34 nRight 54 nTop 100 nBottom 300

清单文件中的类型定义的工作方式类似于 C/C++ typedef。 例如,以下语句将 PLONG 定义为指向 LONG 的指针:

typedef LONG *PLONG;

已在 Main.h 中声明了大多数基本 typedef。 只需添加特定于组件的 typedef。 结构定义的格式与 C/C++ 结构类型相同。

有四种特殊类型:值、掩码、GUID 和COM_INTERFACE_PTR。

值类型
值是按用户可读标签划分的基本类型。 大多数函数文档仅引用函数中使用的特定常量的 #define 值。 例如,大多数程序员不知道 GetLastError 返回的所有代码的实际值是什么,因此在 LogViewer 中看到一个神秘的数值毫无帮助。 清单值通过允许值声明来克服此问题,如以下示例所示:

value LONG ChangeNotifyFlags
{
#define SHCNF_IDLIST      0x0000        // LPITEMIDLIST
#define SHCNF_PATHA       0x0001        // path name
#define SHCNF_PRINTERA    0x0002        // printer friendly name
#define SHCNF_DWORD       0x0003        // DWORD
#define SHCNF_PATHW       0x0005        // path name
#define SHCNF_PRINTERW    0x0006        // printer friendly name
};

这将声明派生自 LONG 的名为“ChangeNotifyFlags”的新类型。 如果将其用作函数参数,将显示人类可读别名,而不是原始数字。

掩码类型
与值类型类似,掩码类型是一种基本类型 (通常为 DWORD) ,它针对每个具有意义的位划分为人类可读的标签。 请参见以下示例:

mask DWORD DirectDrawOptSurfaceDescCapsFlags
{
#define DDOSDCAPS_OPTCOMPRESSED                 0x00000001
#define DDOSDCAPS_OPTREORDERED                  0x00000002
#define DDOSDCAPS_MONOLITHICMIPMAP              0x00000004
};

这会声明一个派生自 DWORD 的新类型,如果用作函数参数,则会在 LogViewer 中为用户细分各个值。 因此,如果值为 0x00000005,LogViewer 将显示:

DDOSDCAPS_OPTCOMPRESSED | DDOSDCAPS_MONOLITHICMIPMAP

GUID 类型
GUID 是 COM 中广泛使用的 16 字节全局唯一标识符。 它们通过两种方式声明:

struct __declspec(uuid("00020400-0000-0000-C000-000000000046")) IDispatch;

class __declspec(uuid("11219420-1768-11D1-95BE-00609797EA4F")) ShellLinkObject;

第一种方法用于 (IID) 声明接口标识符。 LogViewer 显示时,“IID_”将追加到显示名称的开头。 第二种方法用于 (CLSID) 声明类标识符。 LogViewer 将“CLSID_”追加到显示名称的开头。

如果 GUID 类型是函数的参数,则 LogViewer 会将该值与所有声明的 IID 和 CLSID 进行比较。 如果找到匹配项,它将显示 IID 友好名称。 否则,它将以标准 GUID 表示法显示 32 个十六进制字符值。

COM_INTERFACE_PTR类型
COM_INTERFACE_PTR类型是 COM 接口指针的基类型。 声明 COM 接口时,实际上是在定义派生自 COM_INTERFACE_PTR 的新类型。 因此,指向此类类型的指针可以是函数的参数。 如果COM_INTERFACE_PTR基本类型声明为函数的 OUT 参数,并且有一个单独的参数具有 [iid] 标签,则记录器会将在 IID 中传递的 与所有已声明的 GUID 进行比较。 如果存在匹配项,并且声明了与 IID 同名的 COM 接口,记录器将挂钩该接口中的所有函数并记录它们。

以下是示例:

STDAPI CoCreateInstance(
  REFCLSID rclsid,     //Class identifier (CLSID) of the object
  LPUNKNOWN pUnkOuter, //Pointer to controlling IUnknown
  CLSCTX dwClsContext, //Context for running executable code
  [iid] REFIID riid,   //Reference to the identifier of the interface
  [out] COM_INTERFACE_PTR * ppv
                       //Address of output variable that receives 
                       //the interface pointer requested in riid
);

在此示例中, riid 具有 [iid] 修饰符。 这向 Logger 指示 ppv 中返回的指针是由 riid 标识的接口的 COM 接口指针。

还可以声明函数,如下所示:

DDRESULT DirectDrawCreateClipper( DWORD dwFlags, [out] LPDIRECTDRAWCLIPPER *lplpDDClipper, IUnknown *pUnkOuter );

在此示例中,LPDIRECTDRAWCLIPPER 定义为指向 IDirectDrawClipper 接口的指针。 由于 Logger 可以标识 lplpDDClipper 参数中返回的接口类型,因此不需要对任何其他参数使用 [iid] 修饰符。