ReactOS 图形系统分析(51):元文件子系统 --- metafile.c
1. 概述
1.1 什么是元文件
元文件(Metafile,WMF,Windows Metafile)是一种"矢量化的绘图指令序列"。它不像位图那样保存每个像素的颜色值,而是把一次会话中对 GDI 的绘图调用(画线、画矩形、输出文本、设置映射模式......)逐条记录下来,形成一串 METARECORD(元文件记录)。之后可以在任意 DC 上"重放"(PlayMetaFile)这串记录,从而在别的设备(屏幕、打印机、另一个位图 DC)上复现同样的画面。
元文件的典型应用场景:
| 场景 | 说明 |
|---|---|
| 剪贴板数据交换 | 把绘图结果以"矢量"形式放进剪贴板(CF_METAFILEPICT),目标程序可无损缩放重绘 |
| 打印机假脱机 | 把打印作业记录为元文件,稍后交给打印驱动重放 |
| 图元库/控件 | 需要"记忆"一段绘制过程以便反复重放 |
| EMF↔WMF 转换 | 增强型元文件(EMF)可降级转换为传统 WMF 格式 |
1.2 内核侧文件的作用
metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 位于 win32ss/gdi/ntgdi/,是 win32k.sys 中与元文件相关的系统服务入口 集合。它只实现了 4 个 NtGdi* 服务函数,其中 3 个已实现、1 个是未实现的桩。它的定位非常"轻":真正的元文件录制与播放完全在用户态 gdi32.dll 完成,内核侧只负责:
- 提供一个"元文件 DC"类型的内核 DC 句柄(
NtGdiCreateMetafileDC,实际是一个 ALT DC); - 维护"服务器元文件对象"------一个内核 GDI 对象,用于在跨进程场景(主要是剪贴板)中保存/传递元文件的原始字节(
NtGdiCreateServerMetaFile/NtGdiGetServerMetaFileBits); - 预留一个坐标换算入口(
NtGdiConvertMetafileRect,目前是UNIMPLEMENTED桩)。
1.3 设计动机:为什么播放/录制要放在用户态
Windows 的元文件机制从 16 位时代延续而来。传统 WMF 的记录格式(META_* 函数码、16 位坐标、LOGPEN16/LOGFONT16 等 16 位对象描述)带有浓重的 16 位遗产色彩。把录制和播放放在用户态 gdi32 中的好处:
- 无需每次 GDI 调用都陷入内核 :录制时 gdi32 的"元文件物理驱动"(mfdrv)在用户态把 GDI 调用组装成
METARECORD,直接写内存缓冲或磁盘文件,几乎不产生系统调用; - 格式兼容由 gdi32 负责 :16 位坐标转换、ANSI 字符串、
LOGBRUSH16等兼容性细节不需要内核关心; - 内核只保存"数据容器" :服务器元文件对象本质上是一个"带标签的字节缓冲"(
iType+ 可变长数据),内核用统一的对象机制(GDIOBJ_AllocObjWithHandle)管理其生命周期、进程归属与引用计数,具体字节含义由用户态解析。
1.4 总体架构图
应用(记录) 应用(播放/转换)
│ │
CreateMetaFileW ─────┤ PlayMetaFile / EnumMetaFile
CloseMetaFile ───────┤ │
▼ ▼
┌─────────────────────────────┐ ┌──────────────────────────┐
│ gdi32.dll(用户态) │ │ gdi32.dll(用户态) │
│ │ │ │
│ MFDRV_* 物理驱动 │ │ get_metafile_bits() │
│ (wine/mfdrv/init.c 等) │ │ → 循环 METARECORD │
│ · MFDRV_WriteRecord │ │ → PlayMetaFileRecord() │
│ · MFDRV_MetaParam0/1/2/4 │ │ → 普通 GDI API │
│ /6/8(组装记录) │ │ │
│ · 内存:METARECORD 追加 │ └──────────┬───────────────┘
│ · 磁盘:WriteFile │ │ GDI 调用
└─────────────┬───────────────┘ ▼
│ (仅剪贴板/EMF转换场景) ┌─────────────────────┐
│ NtGdi* 系统服务 │ win32k.sys(内核) │
▼ │ │
┌─────────────────────────────┐ │ · DCTYPE_INFO ALT DC│
│ 内核元文件对象 METATYPEOBJ │ │ · META_TYPE 对象 │
│ (metafile.c + gdiobj.c) │ └─────────────────────┘
└─────────────────────────────┘
1.5 内核/用户态分工总表
| 职责 | 所在层 | 实现 |
|---|---|---|
| 录制:GDI 调用 → METARECORD | 用户态 gdi32 | wine/mfdrv/*.c(MFDRV_* 系列) |
| 录制:记录缓冲/磁盘文件管理 | 用户态 gdi32 | MFDRV_WriteRecord、CreateMetaFileW、CloseMetaFile |
| 播放:METARECORD → GDI 调用 | 用户态 gdi32 | PlayMetaFile、PlayMetaFileRecord |
| 元文件 DC 的内核句柄 | 内核 win32k | NtGdiCreateMetafileDC → GreCreateCompatibleDC(hdc, TRUE) |
| 服务器元文件对象(剪贴板用) | 内核 win32k | NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits |
| 元文件对象句柄/生命周期管理 | 内核 win32k | gdiobj.c(GDIObjType_META_TYPE = 0x15) |
| 坐标换算桩 | 内核 win32k | NtGdiConvertMetafileRect(UNIMPLEMENTED) |
2. 核心数据结构
2.1 METAHEADER(wingdi.h)
METAHEADER 定义在 sdk/include/psdk/wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h)(约 2754~2763 行),用 pshpack2(2 字节对齐)打包,共 18 字节 = 9 个 WORD:
c
#include "pshpack2.h"
typedef struct tagMETAHEADER {
WORD mtType; /* 元文件类型:METAFILE_MEMORY(1) 或 METAFILE_DISK(2) */
WORD mtHeaderSize; /* 头大小(以 WORD 为单位),恒为 sizeof(METAHEADER)/2 = 9 */
WORD mtVersion; /* 版本号,MFVERSION = 0x0300 */
DWORD mtSize; /* 整个元文件(头+所有记录)的字节数 / 2(WORD 数) */
WORD mtNoObjects; /* 元文件中创建的 GDI 对象数(画笔/画刷/字体/调色板等) */
DWORD mtMaxRecord; /* 最大的单条记录大小(WORD 数) */
WORD mtNoParameters; /* 未用,恒为 0 */
} METAHEADER,*PMETAHEADER,*LPMETAHEADER;
#include "poppack.h"
字段含义要点:
mtSize是"总长度 ÷ 2",因为元文件记录的大小均以 WORD 为单位表达;mtHeaderSize恒等于 9,播放器从mtHeaderSize * 2 = 18字节处开始遍历记录;- 内存型元文件 =
METAHEADER+ 一串连续METARECORD(文件型元文件的磁盘镜像也把mtType写成METAFILE_MEMORY,见 gdi32/wine/metafile.c 顶部注释); - 磁盘型元文件在
METAHEADER之后还跟一个METAHEADERDISK扩展结构(见 2.5)。
2.2 METARECORD(wingdi.h)
c
typedef struct tagMETARECORD {
DWORD rdSize; /* 记录总大小(WORD 数),含 rdSize 自身 */
WORD rdFunction; /* 记录类型(META_* 函数码) */
WORD rdParm[1]; /* 参数区(可变长,rdSize-3 个 WORD) */
} METARECORD,*PMETARECORD,*LPMETARECORD;
记录机制约定(录制与播放必须一致):
- 每条记录最小 3 个 WORD(rdSize = 3):
rdSize+rdFunction,无参数; - 参数逆序存放 :
rdParm[0]保存调用参数中的最后一个 ,rdParm[1]保存倒数第二个,依此类推。这是因为 16 位元文件的设计把所有参数当 WORD 压栈/存表,播放时按逆序弹出。例如MFDRV_MoveTo(dev, x, y)生成rdParm[0]=y, rdParm[1]=x,播放端MoveToEx(hdc, rdParm[1], rdParm[0]); - 32 位坐标(LONG)会被拆成两个 WORD(低位在前、高位在后)存放;
- 记录以
META_EOF(META_EOF定义见 wingdi.h)结束。
META_* 函数码从 0x01 到 0xFFF,按"所需参数个数"编码(高位半字节是参数 WORD 数)。常见的有:
| 记录 | 值 | 参数数 | 说明 |
|---|---|---|---|
| META_SAVEDC | 0x001E | 0 | 保存 DC 状态 |
| META_RESTOREDC | 0x0127 | 1 | 恢复 DC 状态 |
| META_SETMAPMODE | 0x0103 | 1 | 设置映射模式 |
| META_SETBKCOLOR | 0x0201 | 2 | 背景色(两个 WORD 拼 COLORREF) |
| META_MOVETO | 0x0214 | 2 | 移动当前位置 |
| META_LINETO | 0x0213 | 2 | 画线 |
| META_RECTANGLE | 0x0418 | 4 | 画矩形(left/top/right/bottom) |
| META_ELLIPSE | 0x0418→0x418 | 4 | 画椭圆(注意 META_ELLIPSE 实为 0x418) |
| META_POLYGON | 0x0324 | 变长 | 多边形(count + POINTS 数组) |
| META_POLYLINE | 0x0325 | 变长 | 折线 |
| META_POLYPOLYGON | 0x0538 | 变长 | 复合多边形 |
| META_ARC | 0x0817 | 8 | 圆弧 |
| META_PIE | 0x081A | 8 | 饼图 |
| META_CHORD | 0x0830 | 8 | 弦 |
| META_EXTTEXTOUT | 0x0A32 | 变长 | 扩展文本输出 |
| META_CREATEPENINDIRECT | 0x02FA | 变长 | 创建画笔(LOGPEN16) |
| META_CREATEFONTINDIRECT | 0x02FB | 变长 | 创建字体(LOGFONT16) |
| META_CREATEBRUSHINDIRECT | 0x02FC | 变长 | 创建画刷(LOGBRUSH16) |
| META_CREATEREGION | 0x06FF | 变长 | 创建区域 |
| META_DELETEOBJECT | 0x01F0 | 1 | 删除对象(按句柄表索引) |
| META_SELECTOBJECT | 0x012D | 1 | 选入对象(按句柄表索引) |
| META_BITBLT | 0x0922 | 变长 | 位块传输 |
| META_DIBSTRETCHBLT | 0x0B41 | 变长 | DIB 拉伸传输 |
| META_EOF | 0x0000 | 0 | 元文件结束 |
2.3 内核服务器元文件对象:METATYPEOBJ(metafile.c 内部)
metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 第 17~27 行定义了仅供本文件使用的结构:
c
// Internal Use Only
typedef struct _METATYPEOBJ
{
BASEOBJECT BaseObject; /* GDI 对象头(句柄/引用计数/所有者等) */
DWORD iType; /* 数据标签:GDITAG_TYPE_EMF 或 GDITAG_TYPE_MFP */
DWORD mm; /* 映射模式(METAFILEPICT.mm 的透传保存) */
DWORD xExt; /* 推荐显示宽度(METAFILEPICT.xExt) */
DWORD yExt; /* 推荐显示高度(METAFILEPICT.yExt) */
ULONG cjData; /* 元文件原始数据字节数 */
PBYTE pjData[4]; /* 占位符:实际数据紧跟结构体尾部 */
} METATYPEOBJ, *PMETATYPEOBJ;
要点:
- 该对象不是独立分配的"数据块 + 指针",而是把原始数据整体追加在结构体后面 :分配时请求
sizeof(METATYPEOBJ) + cjData字节(见NtGdiCreateServerMetaFile),pjData数组只是充当"尾部数据起始地址"的锚点,RtlCopyMemory(pmto->pjData, pjData, cjData)直接拷贝到对象内存末尾; iType是区分数据格式的标签,取自 ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h):#define GDITAG_TYPE_EMF 'XEFM'("MFEX" 反读 = MetaFile EXtended,即 EMF 数据)#define GDITAG_TYPE_MFP '_PFM'("MFP" = MetaFile Picture,即 METAFILEPICT 承载的传统 WMF)
mm/xExt/yExt三个字段是为METAFILEPICT(剪贴板CF_METAFILEPICT格式的描述头)透传保存的"推荐显示方式",录制/读取时原样保存、原样取回;- 内核 GDI 对象类型为
GDIObjType_META_TYPE = 0x15,低位句柄类型GDILoObjType_LO_META_TYPE = 0x150000(ntgdityp.h 第 141、166 行)。
2.4 DC 类型与元文件 DC(dc.h)
ntgdi/dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h) 第 39~44 行定义了内核 DC 的三种类型:
c
typedef enum _DCTYPE
{
DCTYPE_DIRECT = 0, /* Normal device context 普通设备上下文 */
DCTYPE_MEMORY = 1, /* Memory device context 内存设备上下文(兼容 DC) */
DCTYPE_INFO = 2, /* Information context 信息上下文(查询设备能力用) */
} DCTYPE;
NtGdiCreateMetafileDC 创建的是 DCTYPE_INFO 类型 的"替代 DC"(ALT DC,句柄类型 GDILoObjType_LO_ALTDC_TYPE = 0x210000)。这类 DC 不绑定真实表面(pSurface 为空),主要用于承载用户态 EMFDC 的设备能力查询与记录上下文。DC 结构(dc.h 第 95~137 行)中的 dctype 字段记录该类型;DCLEVEL(第 49~89 行)保存 DC 的完整状态快照(画笔/画刷/字体/剪裁区/映射矩阵等),元文件播放时通过 SaveDC/RestoreDC 记录保护现场。
与元文件相关的主要句柄类型(ntgdityp.h 第 155~174 行):
| 类型常量 | 值 | 说明 |
|---|---|---|
| GDILoObjType_LO_META_TYPE | 0x150000 | 内核服务器元文件对象 |
| GDILoObjType_LO_ALTDC_TYPE | 0x210000 | 替代 DC(元文件/EMF DC 的内核底座) |
| GDILoObjType_LO_METADC16_TYPE | 0x660000 | 16 位元文件 DC(用户态 mfdrv 的 DC 魔数) |
| GDILoObjType_LO_METAFILE_TYPE | 0x460000 | 用户态 HMETAFILE |
| GDILoObjType_LO_METAFILE16_TYPE | 0x260000 | 16 位元文件句柄 |
2.5 用户态相关结构
METAFILEDRV_PDEVICE(gdi32/wine/mfdrv/metafiledrv.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h) 第 33~40 行):
c
typedef struct
{
struct gdi_physdev dev; /* 物理设备基类(含 hdc、函数表 funcs 等) */
METAHEADER *mh; /* 元文件头指针(内存型时也是记录缓冲的起始) */
UINT handles_size, cur_handles; /* 句柄表容量/当前占用 */
HGDIOBJ *handles; /* GDI 对象句柄表(录制期创建的对象) */
HANDLE hFile; /* 磁盘型元文件的文件句柄 */
} METAFILEDRV_PDEVICE;
这是 gdi32 侧"元文件物理驱动"的设备对象,嵌入在 Wine DC(DC,魔数 OBJ_METADC)的物理设备链上。
HANDLETABLE(wingdi.h 第 2800~2802 行):播放时用于把记录中的"对象索引"映射为真实 GDI 句柄:
c
typedef struct tagHANDLETABLE {
HGDIOBJ objectHandle[1]; /* 实际长度由 mtNoObjects 决定 */
} HANDLETABLE,*PHANDLETABLE, *LPHANDLETABLE;
METAHEADERDISK(gdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c) 第 70~76 行,pshpack1 打包):磁盘型元文件紧随 METAHEADER 之后的扩展块,保存原始磁盘文件路径:
c
typedef struct
{
DWORD dw1, dw2, dw3;
WORD w4;
CHAR filename[0x100]; /* 磁盘元文件的路径(ANSI) */
} METAHEADERDISK;
常量:METAFILE_MEMORY = 1、METAFILE_DISK = 2、MFVERSION = 0x300(gdi32/wine/gdi_private.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/gdi_private.h) 第 74~77 行)。
2.6 METAFILEPICT:剪贴板格式描述头
METAFILEPICT 是剪贴板 CF_METAFILEPICT 格式的数据头,定义于 wingdi.h,紧随其后的才是元文件本身。它是连接"内核服务器元文件对象"与剪贴板的关键------NtGdiCreateServerMetaFile 的 mm/xExt/yExt 三个参数正是从它透传:
c
typedef struct tagMETAFILEPICT
{
LONG mm; /* 推荐映射模式(MM_*,如 MM_ANISOTROPIC) */
LONG xExt; /* 推荐显示宽度(MM_ISOTROPIC/ANISOTROPIC 时的比例基准) */
LONG yExt; /* 推荐显示高度 */
HMETAFILE hMF;/* 元文件句柄(本结构的元文件部分) */
} METAFILEPICT;
使用它的两个 ReactOS 函数:
GdiCreateLocalMetaFilePict(hmo):内核服务器元文件对象 →HGLOBAL(METAFILEPICT + WMF 数据),供放进剪贴板;GdiConvertMetaFilePict(hMetaFilePict):剪贴板里的 METAFILEPICT → 内核服务器元文件对象(GDITAG_TYPE_MFP)。
它们分别对应"内核 → 用户态本地对象"与"用户态本地对象 → 内核"两个方向,实现剪贴板数据的会话内中转。
2.7 OBJ_* 用户态对象魔数与内核类型的对应
| 用户态魔数(gdi32) | 对应内核句柄类型 | 说明 |
|---|---|---|
| OBJ_METADC | GDILoObjType_LO_METADC16_TYPE (0x660000) | 16 位元文件录制 DC(mfdrv) |
| OBJ_METAFILE | GDILoObjType_LO_METAFILE_TYPE (0x460000) | HMETAFILE |
| OBJ_ENHMETADC | GDILoObjType_LO_ALTDC_TYPE (0x210000) | EMF 录制 DC 的内核底座(NtGdiCreateMetafileDC) |
| OBJ_ENHMETAFILE | (用户态 EMF 句柄) | HENHMETAFILE,录制在用户态 EMR 缓冲 |
| (无 OBJ) | GDIObjType_META_TYPE (0x15) | 内核服务器元文件对象(剪贴板桥) |
3. 内核系统服务函数逐一展开
本节逐个分析 metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 中的 4 个 NtGdi* 服务函数及其依赖的内核辅助机制。
3.1 NtGdiCreateMetafileDC ------ 创建元文件 DC(ALT DC)
签名:
c
HDC
APIENTRY
NtGdiCreateMetafileDC(IN HDC hdc)
参数:
| 参数 | 含义 |
|---|---|
| hdc | 参考 DC 句柄。源码中该参数实际上未参与分支判断(原本计划"非空则参考、空则取桌面 DC",该逻辑被注释掉) |
返回值: 新 DC 句柄(ALT DC),失败返回 NULL。
实现流程(metafile.c 第 35~44 行):
c
HDC
APIENTRY
NtGdiCreateMetafileDC(IN HDC hdc)
{
//if (hdc)
/* Call the internal function to create an alternative info DC */
return GreCreateCompatibleDC(hdc, TRUE);
// No HDC call NtUser.
//return UserGetDesktopDC(DCTYPE_INFO, TRUE, FALSE);
}
- 无论
hdc是否为空,都调用内部函数GreCreateCompatibleDC(hdc, TRUE); - 被注释掉的
UserGetDesktopDC(DCTYPE_INFO, TRUE, FALSE)提示了原设计意图:hdc 为空时应从桌面 DC 派生一个 DCTYPE_INFO 的信息上下文。
核心调用 GreCreateCompatibleDC(hdc, bAltDc=TRUE)(ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c) 第 817~880 行):
- 若
hdc非空:DC_LockDc(hdc)锁定源 DC,取其ppdev(物理设备对象)并PDEVOBJ_vReference引用,然后解锁; - 若
hdc为空:EngpGetPDEV(NULL)获取默认显示设备(display PDEV); - 依据
bAltDc选择句柄类型:bAltDc ? GDILoObjType_LO_ALTDC_TYPE : GDILoObjType_LO_DC_TYPE,调用DC_AllocDcWithHandle分配新 DC; DC_vInitDc(pdcNew, bAltDc ? DCTYPE_INFO : DCTYPE_MEMORY, ppdev)初始化:元文件 DC 的dctype = DCTYPE_INFO(信息上下文),普通兼容 DC 的dctype = DCTYPE_MEMORY;DC_InitHack(pdcNew)(代码注释 FIXME: HACK!)与DC_bAllocDcAttr补齐属性块;- 解锁并返回
pdcNew->BaseObject.hHmgr。
使用方式: gdi32 在创建 EMF DC(增强元文件 DC)时调用它。见 gdi32/wine/rosglue.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/rosglue.c) 第 121~136 行 alloc_dc_ptr(OBJ_ENHMETADC):先 NtGdiCreateMetafileDC(NULL) 拿到内核 ALT DC 作为 Wine DC 的 hdc 底座,再 GdiSetLDC 把用户态 WINEDC 挂到该句柄上,iType = LDC_EMFLDC。gdi32/wine/emfdc.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/emfdc.c) 第 2401 行也保留了对 NtGdiCreateMetafileDC(hdc) 的(注释掉的)引用。
注意事项:
- 该函数创建的内核 DC 只是"记录上下文容器",本身不含任何元文件字节;真正的 EMF 记录由用户态
emfdc.c(EMR*记录)负责; - 与 16 位
CreateMetaFile(gdi32 mfdrv,返回OBJ_METADC)不同,这里不建立元文件缓冲; - 被注释的
UserGetDesktopDC分支意味着 ReactOS 当前对空 hdc 走EngpGetPDEV(NULL),语义与 Windows 略有差异(不影响基本功能)。
3.2 NtGdiCreateServerMetaFile ------ 创建服务器元文件对象
签名:
c
HANDLE
APIENTRY
NtGdiCreateServerMetaFile(
IN DWORD iType, /* 数据标签:GDITAG_TYPE_EMF / GDITAG_TYPE_MFP */
IN ULONG cjData, /* 数据字节数 */
IN PBYTE pjData, /* 数据指针(用户态地址) */
IN DWORD mm, /* METAFILEPICT.mm 透传 */
IN DWORD xExt, /* METAFILEPICT.xExt 透传 */
IN DWORD yExt /* METAFILEPICT.yExt 透传 */
)
返回值: 成功返回内核 GDI 句柄(hmo),失败返回 NULL。
实现流程(metafile.c 第 49~100 行):
- 入参校验 :只有
iType == GDITAG_TYPE_EMF || iType == GDITAG_TYPE_MFP且cjData && pjData才继续,否则直接返回 NULL(既不允许空数据,也不允许未知标签); - 分配对象 :
GDIOBJ_AllocObjWithHandle(GDILoObjType_LO_META_TYPE, sizeof(METATYPEOBJ) + cjData)------ 对象总大小为结构体加上原始数据,一次分配; - 填充字段 :
pmto->iType/mm/xExt/yExt/cjData直接赋值; - 安全拷贝 :
_SEH2_TRY内先ProbeForRead(pjData, cjData, 1)探测用户缓冲区可读,再RtlCopyMemory(pmto->pjData, pjData, cjData)把数据拷进对象尾部;任何异常进入_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)把Pass置 FALSE(防止蓝屏); - 设置所有者并解锁 :成功则
GDIOBJ_vSetObjectOwner(&pmto->BaseObject, GDI_OBJ_HMGR_PUBLIC)------ 把对象标记为公共句柄 (GDI_OBJ_HMGR_PUBLIC允许其他进程通过句柄访问),随后GDIOBJ_vUnlockObject释放创建期的排他锁,返回pmto->BaseObject.hHmgr; - 失败清理 :拷贝失败则
GDIOBJ_vDeleteObject(&pmto->BaseObject)撤销对象。
依赖的内核机制(gdiobj.c):
GDIOBJ_AllocObjWithHandle(ObjectType, cjSize)(gdiobj.c 第 1535 行):从ObjectType高 16 位提取对象类型objt,调用GDIOBJ_AllocateObject分配对象体,再GDIOBJ_hInsertObject插入句柄表,返回对象指针(内含句柄hHmgr);GDIOBJ_vSetObjectOwner(pobj, GDI_OBJ_HMGR_PUBLIC):把句柄表项的所有者设为公共,使句柄可被其他进程使用(剪贴板跨进程传递的关键);- 清理:
gdiobj.c的apfnCleanup[GDIObjType_META_TYPE] = GDIOBJ_vCleanup(第 181 行),进程退出时GDI_CleanupForProcess会按所有者回收所有 META 对象。
使用方式(两个调用方都在 gdi32):
- gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 60 行
GdiConvertEnhMetaFile:NtGdiCreateServerMetaFile(GDITAG_TYPE_EMF, nSize, Buffer, 0, 0, 0)------ 把 EMF 位数据存进内核; - gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 232 行
GdiConvertMetaFilePict:NtGdiCreateServerMetaFile(GDITAG_TYPE_MFP, nSize, Buffer, pInfo->mm, pInfo->xExt, pInfo->yExt)------ 把METAFILEPICT里的传统 WMF 数据连同推荐尺寸存进内核。
注意事项:
GDI_OBJ_HMGR_PUBLIC的语义是"公开句柄":句柄值本身可以被传递,但NtGdiGetServerMetaFileBits读取时仍要求调用进程与对象在同一会话(会话隔离由 win32k 句柄表保证);- 数据是一次性拷贝进内核的,调用方之后可以释放自己的缓冲;
- 只接受 EMF/MFP 两种标签,其它格式一律拒绝。
3.3 NtGdiGetServerMetaFileBits ------ 取服务器元文件位数据
签名:
c
ULONG
APIENTRY
NtGdiGetServerMetaFileBits(
IN HANDLE hmo, /* 服务器元文件句柄 */
IN ULONG cjData, /* 请求的字节数(0 表示仅查询大小) */
OUT OPTIONAL PBYTE pjData, /* 输出缓冲 */
OUT PDWORD piType, /* 输出 iType */
OUT PDWORD pmm, /* 输出 mm */
OUT PDWORD pxExt, /* 输出 xExt */
OUT PDWORD pyExt /* 输出 yExt */
)
返回值: 成功返回可用的数据字节数(cjData == 0 时为总大小;cjData != 0 时为 pmto->cjData),失败或参数不匹配返回 0。
实现流程(metafile.c 第 105~165 行):
GDIOBJ_LockObject(hmo, GDIObjType_META_TYPE)锁定对象(gdiobj.c 第 833 行:校验句柄高 16 位类型 == 0x15、引用句柄表项、获取排他锁);锁定失败返回 0;- 校验
iType必须是GDITAG_TYPE_EMF或GDITAG_TYPE_MFP,否则返回 0; cjRet = pmto->cjData(先返回"需要的大小");- 若
cjData != 0:要求恰好等于pmto->cjData(即一次性取完,不允许部分读取;大小不符则cjRet = 0); - 在
_SEH2_TRY中依次ProbeForWrite探测piType/pmm/pxExt/pyExt/pjData五个用户缓冲,然后写入iType/mm/xExt/yExt并RtlCopyMemory(pjData, pmto->pjData, cjData)拷出数据;任何异常使cjRet = 0; GDIOBJ_vUnlockObject解锁,返回cjRet。
使用方式(两段式调用约定): 调用方先以 cjData=0, pjData=NULL 查询总大小,再分配缓冲、以精确大小第二次调用取数据。典型调用见:
- gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 81/91 行(
GdiCreateLocalEnhMetaFile:先查大小再取数据并解出 iType/mm/xExt/yExt); - gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 115/123 行(
GdiCreateLocalMetaFilePict同样两段式)。
注意事项:
- 输出参数全部可选(
piType/pmm/pxExt/pyExt可为 NULL,pjData可为 NULL); ProbeForWrite只探测不捕获后续访问;真正写入仍在_SEH2_TRY内完成,故非法地址不会导致内核崩溃;- "必须整取"的限制意味着该接口不适合流式/增量读取,这与服务器元文件对象"整块快照"的定位一致。
3.4 NtGdiConvertMetafileRect ------ 矩形坐标转换(未实现)
签名:
c
LONG
APIENTRY
NtGdiConvertMetafileRect(IN HDC hDC, IN OUT PRECTL pRect)
实现(metafile.c 第 170~177 行):
c
UNIMPLEMENTED;
return 0;
- 标注为
@unimplemented,调用UNIMPLEMENTED宏会在调试串口输出WARNING: ... is UNIMPLEMENTED!并返回 0; - 设计意图(由函数名与参数推断):把
pRect从"元文件记录坐标"换算到"显示/设备坐标"(结合 DC 的映射模式与窗口/视口范围做缩放),供在元文件 DC 上做坐标预换算使用; - 由于 gdi32 的录制/播放全部自行处理坐标(录制时按当前 DC 的逻辑坐标直接记录,播放时在目标 DC 上按逻辑坐标重放),该桩对现有功能无影响。
注意事项:
- 切勿修改此函数的返回语义(调用方 gdi32 目前无任何调用点,属于预留接口);
- 若未来实现,必须正确处理
MM_ISOTROPIC/MM_ANISOTROPIC的窗口/视口比例缩放,并保留SetLastError约定。
3.5 内核对象机制小结
| 机制 | 函数 | 作用 |
|---|---|---|
| 分配 | GDIOBJ_AllocObjWithHandle |
分配对象体并插入句柄表(metafile.c 用它分配 METATYPEOBJ) |
| 加锁 | GDIOBJ_LockObject |
校验类型 → 引用句柄项 → 排他锁(META_TYPE 属排他对象类型,见 gdiobj.c 第 115~119 行 ASSERT_EXCLUSIVE_OBJECT_TYPE) |
| 解锁 | GDIOBJ_vUnlockObject |
释放锁与引用 |
| 所有权 | GDIOBJ_vSetObjectOwner |
设为公共句柄以便跨进程使用 |
| 清理 | apfnCleanup[0x15] = GDIOBJ_vCleanup |
对象销毁/进程退出时的内存回收 |
4. 元文件记录机制(gdi32 mfdrv:GDI 调用 → METARECORD)
4.1 物理驱动架构:gdi_dc_funcs 函数表
gdi32 采用"物理设备链"架构:每个 DC 上挂一串物理驱动,每个驱动实现一组 gdi_dc_funcs 回调。元文件物理驱动(mfdrv)的回调表 MFDRV_Funcs 定义在 mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 100~231 行,优先级 GDI_PRIORITY_GRAPHICS_DRV。
关键点:
- 录制对象、图形、DC 状态、文本、位块传输的入口都被指派为
MFDRV_*函数;查询类(GetDeviceCaps 等)要么桩实现、要么为 NULL(走下一层驱动); - 路径类(BeginPath/EndPath/FillPath/StrokePath/WidenPath 等)全部返回 FALSE(mfdrv/dc.c 第 148~196 行)------传统 WMF 格式没有路径记录,主动失败而不是记录近似数据;
- 查询回调
MFDRV_GetDeviceCaps(init.c 第 85~97 行):TECHNOLOGY返回DT_METAFILE,TEXTCAPS返回 0,其余返回 0; MFDRV_ExtEscape(init.c 第 40~59 行):把 Escape 调用记录成META_ESCAPE记录;需要输出的 Escape(cbOutput非零)直接返回 0(元文件无法承载查询类 Escape)。
4.2 元文件 DC 的创建:MFDRV_AllocMetaFile
mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 238~276 行:
alloc_dc_ptr(OBJ_METADC)分配用户态 Wine DC(魔数 OBJ_METADC,即 16 位元文件 DC,内核侧对应GDILoObjType_LO_METADC16_TYPE);- 分配
METAFILEDRV_PDEVICE与METAHEADER; push_dc_driver(&dc->physDev, &physDev->dev, &MFDRV_Funcs)把 mfdrv 挂上物理设备链;- 初始化句柄表:
handles_size = HANDLE_LIST_INC = 20,cur_handles = 0; - 初始化头部:
mtHeaderSize = 9、mtVersion = 0x0300、mtSize = 9、mtNoObjects = 0、mtMaxRecord = 0、mtNoParameters = 0; SetVirtualResolution(dev.hdc, 0, 0, 0, 0)设置虚拟分辨率(使 GetDeviceCaps 等有合理默认值)。
4.3 CreateMetaFileW / CloseMetaFile / DeleteMetaFile
CreateMetaFileW(init.c 第 319~358 行) :filename 为 NULL 时创建内存型元文件(mtType = METAFILE_MEMORY);非 NULL 时创建磁盘型元文件(mtType = METAFILE_DISK),先 CreateFileW 打开文件并把初始 METAHEADER 写盘,再用 MF_CreateMetaHeaderDisk 扩展头部追加文件名(METAHEADERDISK)。CreateMetaFileA 先把 ANSI 路径转宽字符再调用 W 版。
MFDRV_CloseMetaFile(init.c 第 388~438 行):
- 校验对象类型为 OBJ_METADC、引用计数为 1;
- 写入
META_EOF结束记录(MFDRV_MetaParam0(dc->physDev, META_EOF),对应 MSDN KB Q99334 的要求); - 磁盘型:把文件指针移到开头,把
mtType临时改成 METAFILE_MEMORY 后回写头部(这是 Windows 的行为------磁盘元文件头里的类型也写 MEMORY,便于统一读取),随后关闭文件、改回 METAFILE_DISK。
CloseMetaFile(init.c 第 452~467 行) :调用 MFDRV_CloseMetaFile 后,把 METAHEADER 包装成 HMETAFILE(MF_Create_HMETAFILE → alloc_gdi_handle(mh, OBJ_METAFILE, NULL),见 gdi32/wine/metafile.c 第 107~110 行),并把 physDev->mh 置 NULL 避免二次释放。
DeleteMetaFile(gdi32/wine/metafile.c 第 139~144 行) :free_gdi_handle(hmf) 释放 GDI 句柄,再 HeapFree 释放 METAHEADER 缓冲。
4.4 记录写入核心:MFDRV_WriteRecord
mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 475~511 行,是所有 MFDRV_* 记录的汇聚点:
c
BOOL MFDRV_WriteRecord( PHYSDEV dev, METARECORD *mr, DWORD rlen)
- 内存型 :
len = mh->mtSize*2 + rlen;若超过当前HeapSize,按size += size/2 + rlen扩容(HeapReAlloc,注意函数头注释"会改变 mh 指针");随后memcpy((WORD*)mh + mh->mtSize, mr, rlen)把记录追加到头部之后; - 磁盘型 :直接
WriteFile(hFile, mr, rlen, ...)追加到文件; - 未知类型打印
ERR("Unknown metafile type"); - 写完后更新
mh->mtSize += rlen/2与mh->mtMaxRecord = max(...)。
4.5 通用参数记录构造:MFDRV_MetaParam0/1/2/4/6/8
mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 518~628 行,6 个函数分别构造 0/1/2/4/6/8 参数记录。统一模式:在栈上 分配 char 缓冲(8/10/14/18/22 字节),按 rdSize = 3 + 参数个数(WORD 数)填充 rdFunction,然后逆序填入参数:
c
/* MFDRV_MetaParam4:rdParm[0]=param4, rdParm[1]=param3, ... */
mr->rdSize = 7;
mr->rdFunction = func;
params[0] = param4;
params[1] = param3;
params[2] = param2;
params[3] = param1;
return MFDRV_WriteRecord( dev, mr, mr->rdSize * 2);
这与 2.2 节的"参数逆序存放"约定一致,播放端按逆序还原。
4.6 对象句柄表管理(mfdrv/objects.c)
元文件内的 GDI 对象通过"句柄表索引"引用,而不是直接存句柄值(句柄值跨进程/跨机器无意义)。管理函数:
| 函数 | 行为 |
|---|---|
MFDRV_AddHandle(dev, obj)(第 39 行) |
在 handles\[\] 找空槽(容量不足按 HANDLE_LIST_INC=20 扩容),存入 get_full_gdi_handle(obj),递增 cur_handles 并同步 mh->mtNoObjects(注意注释:索引 0 在元文件中不保留为特殊值) |
MFDRV_RemoveHandle(dev, index)(第 64 行) |
清空槽位、递减计数 |
MFDRV_FindObject(dev, obj)(第 81 行) |
线性查找句柄返回索引,找不到返回 -1 |
MFDRV_DeleteObject(dev, obj)(第 98 行) |
写 META_DELETEOBJECT 记录(参数=索引),然后清空句柄槽 |
MFDRV_SelectObject(dev, index)(第 125 行,static) |
写 META_SELECTOBJECT 记录 |
对象惰性创建 :MFDRV_SelectBrush/SelectPen/SelectFont 先 FindObject,找不到才写 META_CREATE*INDIRECT 创建记录并 AddHandle,再写选中记录;同时调用 GDI_hdc_using_object 告知对象被 DC 使用(防止用户态过早删除)。MFDRV_DeleteDC(init.c 第 292~304 行)遍历句柄表对每个对象调用 GDI_hdc_not_using_object。
4.7 图形记录(mfdrv/graphics.c)
- 简单图形 :
MFDRV_MoveTo/LineTo→ MetaParam2;MFDRV_Arc/Pie/Chord→ MetaParam8;MFDRV_Ellipse/Rectangle→ MetaParam4;MFDRV_RoundRect→ MetaParam6;MFDRV_SetPixel→ MetaParam4 且颜色拆为HIWORD(color)/LOWORD(color);MFDRV_ExtFloodFill→ MetaParam4。 - 折线/多边形 :
MFDRV_MetaPoly(第 121 行)把 POINTS(16 位)数组直接拷进记录;MFDRV_Polyline/Polygon先把 32 位 POINT 数组转换为 POINTS(16 位) (逐元素截断);MFDRV_PolyPolygon记录格式为多边形数 + INT16 每多边形点数 + 全部 POINTS。 - 区域记录 :
MFDRV_CreateRegion(第 257 行)用GetRegionData拿到矩形列表,压缩成"带(band)"格式写入META_CREATEREGION记录(格式说明见 gdi32/wine/metafile.c 的MF_Play_MetaCreateRegion注释),然后MFDRV_AddHandle返回索引;MFDRV_PaintRgn/InvertRgn写META_PAINTREGION/META_INVERTREGION,MFDRV_FillRgn/FrameRgn先创建区域和画刷再写META_FILLREGION/META_FRAMEREGION;MFDRV_ExtSelectClipRgn仅支持RGN_COPY(写 SELECT/DELETE 两条记录后 RemoveHandle)。 - 贝塞尔 :
MFDRV_PolyBezier/PolyBezierTo(第 428/438 行)直接返回 FALSE------注释明确说明"元文件不记录贝塞尔曲线,连直线近似都不记"。
4.8 DC 状态记录(mfdrv/dc.c)
全部通过 MetaParam 系列写记录:SaveDC→META_SAVEDC(0 参)、RestoreDC→META_RESTOREDC、SetMapMode→META_SETMAPMODE、SetBkMode/SetROP2/SetRelAbs/SetPolyFillMode/SetStretchBltMode/SetTextCharacterExtra→META_*(1 参)、SetBkColor/SetTextColor→META_*(颜色拆高低字,2 参)、IntersectClipRect/ExcludeClipRect→META_*(4 参)、SetViewportExtEx/OrgEx、SetWindowExtEx/OrgEx、OffsetViewportOrgEx/WindowOrgEx→META_*(2 参)、ScaleViewportExtEx/WindowExtEx→META_*(4 参)、SetTextJustification→META_*(2 参)、SetMapperFlags→META_SETMAPPERFLAGS(拆高低字)。
路径类(AbortPath/BeginPath/CloseFigure/EndPath/FillPath/FlattenPath/SelectClipPath/StrokeAndFillPath/StrokePath/WidenPath)全部返回 FALSE;SetDCBrushColor/SetDCPenColor 返回 CLR_INVALID。
4.9 对象创建记录(mfdrv/objects.c)
- 画笔 :
MFDRV_CreatePenIndirect把LOGPEN16(16 位 LOGPEN)直接拷入META_CREATEPENINDIRECT记录;MFDRV_SelectPen处理两种情况:普通 LOGPEN 直接转换;EXTLOGPEN(扩展画笔)降级为 LOGPEN16(丢弃用户样式等扩展信息,源码 FIXME 注明)。 - 字体 :
MFDRV_CreateFontIndirect把LOGFONTW转成LOGFONT16(面名字符串用WideCharToMultiByte(CP_ACP)转 ANSI 并零填充),写入META_CREATEFONTINDIRECT。 - 画刷 :
MFDRV_CreateBrushIndirect分两类:BS_SOLID/BS_NULL/BS_HATCHED →META_CREATEBRUSHINDIRECT(LOGBRUSH16);BS_PATTERN/BS_DIBPATTERN →META_DIBCREATEPATTERNBRUSH(内嵌 BITMAPINFO + 位图数据,通过get_brush_bitmap_info/get_dib_info_size计算布局,统一转为自底向上 DIB)。 - 调色板 :
MFDRV_SelectPalette通过GetObjectA取条目数,组装 LOGPALETTE 写META_CREATEPALETTE记录,紧接着写META_SELECTPALETTE;MFDRV_RealizePalette写META_REALIZEPALETTE并恒返回 1(无法得知实际映射数)。
4.10 位块传输记录(mfdrv/bitblt.c)
MFDRV_PatBlt:写META_PATBLT(6 参:x/y/w/h + ROP 高低字);MFDRV_StretchBlt(第 47 行):关键约束------不允许把元文件 DC 作为源 DC (devSrc->funcs == devDst->funcs直接返回 FALSE,因为元文件无法引用另一段记录作为位图源)。在STRETCH_VIA_DIB宏下走META_DIBSTRETCHBLT:取源 DC 当前位图,用GetDIBits转成 DIB 嵌入记录(BITMAPINFOHEADER + 调色板 + 像素);否则走META_STRETCHBLT(GetBitmapBits 内嵌位图位)。记录参数布局(rdParm0...9):ROP 低/高字、src h/w/y/x、dst h/w/y/x。MFDRV_SetDIBitsToDevice/StretchDIBits声明于头文件;播放端对应META_SETDIBTODEV/META_STRETCHDIB。
4.11 文本记录(mfdrv/text.c)
MFDRV_ExtTextOut(第 75 行)先把 RECT 转成 RECT16、INT 的 dx 数组转成 INT16 数组、宽字符串转 ANSI(内部 MFDRV_MetaExtTextOut 第 36 行组装 META_EXTTEXTOUT 记录):
记录布局:rdParm[0]=y, [1]=x, [2]=count, [3]=flags,若 flags 含 ETO_OPAQUE|ETO_CLIPPED 则紧跟 RECT16(4 个 SHORT),随后是 ANSI 字符串(对齐到 WORD),末尾可选 INT16 dx 数组。播放端 MF_Play_MetaExtTextOut 按同一布局解析(见 5.2)。
4.12 记录机制小结
| 步骤 | 位置 | 动作 |
|---|---|---|
| 1 | 应用 | 调用任意 GDI API(如 Rectangle) |
| 2 | gdi32 分发 | 按 DC 物理设备链路由到 MFDRV_Rectangle |
| 3 | mfdrv/graphics.c | 组装 META_RECTANGLE 记录(参数逆序) |
| 4 | mfdrv/init.c | MFDRV_WriteRecord 追加到内存缓冲/磁盘文件,更新 mtSize/mtMaxRecord |
| 5 | 应用 | CloseMetaFile 写 META_EOF,返回 HMETAFILE |
4.13 录制端不支持的操作汇总
受 WMF 格式能力限制,mfdrv 对以下操作主动失败(返回 FALSE / 0 / CLR_INVALID),不会写入任何记录:
| 操作类别 | 具体函数 | 原因 |
|---|---|---|
| 路径 | MFDRV_AbortPath/BeginPath/CloseFigure/EndPath/FillPath/FlattenPath/SelectClipPath/StrokeAndFillPath/StrokePath/WidenPath | WMF 没有路径记录类型(EMF 才有 EMR_BEGINPATH 等) |
| 贝塞尔 | MFDRV_PolyBezier/PolyBezierTo | WMF 无贝塞尔记录,也不做直线近似(源码注释明确说明) |
| 复合 DC | MFDRV_CreateCompatibleDC | 元文件 DC 不能作为创建兼容 DC 的源 |
| 颜色扩展 | MFDRV_SetDCBrushColor/SetDCPenColor | WMF 时代没有 DC 画笔/画刷颜色概念 |
| 元文件互操作 | MFDRV_StretchBlt(源为元文件 DC) | devSrc->funcs == devDst->funcs 时拒绝------元文件不能引用另一段记录做位图源 |
| 查询类 Escape | MFDRV_ExtEscape(cbOutput 非零) | 元文件无法承载"要输出数据"的 Escape |
这些桩的存在保证:应用在元文件 DC 上调用不支持的 API 时能立即得知失败(而不是静默丢失绘图),与 Windows 行为一致。
5. 元文件播放(gdi32:METARECORD → GDI 调用)
播放代码在 gdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c)(注意与内核 metafile.c 同名不同目录)。
5.1 数据获取:get_metafile_bits / MF_ReadMetaFile
MF_ReadMetaFile(hfile)(第 152 行):读METAHEADER(18 字节),校验mtType == METAFILE_MEMORY、mtVersion == MFVERSION(0x300)、mtHeaderSize == 9;然后按mtSize*2扩容缓冲并读入全部记录;磁盘型头部mtType若是 DISK 会被改回 MEMORY(第 182~185 行)。get_metafile_bits(hmf)(第 289 行):GDI_GetObjPtr(hmf, OBJ_METAFILE)取 METAHEADER;内存型直接memcpy(mtSize*2)拷贝;磁盘型调用MF_LoadDiskBasedMetaFile(第 243 行:按 METAHEADERDISK 里的路径重新打开文件再 MF_ReadMetaFile)。返回的缓冲需调用方 HeapFree。
5.2 PlayMetaFile ------ 播放主循环
签名: BOOL WINAPI PlayMetaFile(HDC hdc, HMETAFILE hmf)
流程(第 380~451 行):
get_metafile_bits(hmf)取得内存副本;- 保存目标 DC 现场 :
GetCurrentObject保存当前画笔/画刷/调色板;CreateRectRgn+GetClipRgn保存剪裁区; - 按
mh->mtNoObjects分配HANDLETABLE(全零); - 遍历记录 :
offset = mh->mtHeaderSize * 2(跳过头部),循环直到offset >= mh->mtSize * 2:- 取
mr = (METARECORD*)((char*)mh + offset); - 安全检查:
mr->rdSize < 3视为非法记录(记录最小 3 WORD),打印 TRACE 并 break(防畸形元文件越界); offset += mr->rdSize * 2前移;- 遇到
META_EOF停止; PlayMetaFileRecord(hdc, ht, mr, mh->mtNoObjects)分发执行;
- 取
- 恢复现场 :重新选回画笔/画刷/调色板,
ExtSelectClipRgn(hdc, hRgn, RGN_COPY)恢复剪裁区,删除临时区域; - 清理句柄表 :遍历
mtNoObjects个槽位,非零对象DeleteObject;释放 ht 与 mh 缓冲;返回 TRUE。
5.3 PlayMetaFileRecord ------ 单条记录分发
签名: BOOL WINAPI PlayMetaFileRecord(HDC hdc, HANDLETABLE *ht, METARECORD *mr, UINT handles)
第 540~1033 行的大 switch,把每种 META_* 还原成对目标 DC 的 GDI 调用(坐标/颜色按逆序取回并做 SHORT 符号扩展)。分组说明:
| 类别 | 记录 → 调用 |
|---|---|
| DC 状态 | SETBKCOLOR/SETTEXTCOLOR → MAKELONG 拼回 COLORREF;SETMAPMODE/SETBKMODE/SETROP2/SETRELABS/SETPOLYFILLMODE/SETSTRETCHBLTMODE/SETTEXTALIGN/SETTEXTCHAREXTRA/SETTEXTJUSTIFICATION;SAVEDC/RESTOREDC;SETWINDOWORG/EXT、SETVIEWPORTORG/EXT、OFFSET*、SCALE* |
| 坐标/图形 | MOVETO/LINETO、ARC/PIE/CHORD/ELLIPSE/RECTANGLE/ROUNDRECT/PATBLT/SETPIXEL/FLOODFILL/EXTFLOODFILL、POLYGON/POLYLINE(convert_points 把 POINTS 转回 POINT)、POLYPOLYGON(counts 升 32 位) |
| 剪裁 | EXCLUDECLIPRECT/INTERSECTCLIPRECT/OFFSETCLIPRGN/SELECTCLIPREGION |
| 对象 | DELETEOBJECT(删句柄表项)、SELECTOBJECT(按索引选入)、CREATEPENINDIRECT/CREATEFONTINDIRECT/CREATEBRUSHINDIRECT(用 16 位参数重建 LOGPEN/LOGFONTA/LOGBRUSH)、CREATEPALETTE、CREATEPATTERNBRUSH(BS_PATTERN 用 CreateBitmap+CreatePatternBrush;BS_DIBPATTERN 用 CreateDIBPatternBrushPt)、DIBCREATEPATTERNBRUSH、CREATEREGION(MF_Play_MetaCreateRegion 把 band 格式还原为 CombineRgn 序列) |
| 文本 | TEXTOUT(ANSI 字符串按 rdParm[0] 长度解析)、EXTTEXTOUT(MF_Play_MetaExtTextOut,见 4.11 布局逆向解析) |
| 位块 | BITBLT(CreateCompatibleDC+CreateBitmap+BitBlt)、STRETCHBLT(同上+StretchBlt)、DIBBITBLT(rdSize>12 判定有 DIB → StretchDIBits,否则退化为 PatBlt)、DIBSTRETCHBLT、STRETCHDIB、SETDIBTODEV |
| 区域操作 | FILLREGION/FRAMEREGION/INVERTREGION/PAINTREGION(按句柄表索引取 HRGN/HBRUSH) |
| ESCAPE | 过滤 GETSCALINGFACTOR/GETPHYSPAGESIZE/GETPRINTINGOFFSET/SETABORTPROC(返回 FALSE),其余转发 Escape |
| 未实现 | META_UNIMP 宏列出的 13 个:DRAWTEXT、ANIMATEPALETTE、SETPALENTRIES、RESIZEPALETTE、RESETDC、STARTDOC、STARTPAGE、ENDPAGE、ABORTDOC、ENDDOC、CREATEBRUSH、CREATEBITMAPINDIRECT、CREATEBITMAP(打印 Escape 与 16 位位图创建在 WMF 中罕见,FIXME 记录) |
| 未知 | 打印 WARN 并返回 FALSE |
5.4 EnumMetaFile / GetMetaFileBitsEx / SetMetaFileBitsEx
- EnumMetaFile (第 464 行):与 PlayMetaFile 相同的主循环,但不执行记录而是回调
lpEnumFunc(hdc, ht, mr, mh->mtNoObjects, lpData);回调返回 FALSE 即中止并返回 FALSE;同样保存/恢复画笔画刷字体、清理句柄表。 - GetMetaFileBitsEx (第 1091 行):取原始字节。
GDI_GetObjPtr(hmf, OBJ_METAFILE);磁盘型先MF_LoadDiskBasedMetaFile加载;返回mh->mtSize * 2;buf非空时按min(mfSize, nSize)拷出(两段式:buf=NULL 查询大小)。 - SetMetaFileBitsEx (第 1049 行):校验
size为偶数、mtType==METAFILE_MEMORY、mtVersion==MFVERSION、mtHeaderSize==9(失败SetLastError(ERROR_INVALID_DATA)),拷贝整块并mf_out->mtSize = size/2,返回MF_Create_HMETAFILE。
5.5 GetWinMetaFileBits ------ EMF → WMF 转换
(第 1257 行)用"录制 WMF"的方式把 EMF 降级为 WMF:CreateMetaFileW(NULL) 开一个新元文件 DC → add_mf_comment 把 EMF 原始字节作为 MFCOMMENT Escape 记录嵌入 → SetMapMode + set_window(把 ENHMETAHEADER.rclFrame 按映射模式换算为窗口原点/范围,用自实现 muldiv 避免 MulDiv 对负数的差异)→ PlayEnhMetaFile 重放到元文件 DC → CloseMetaFile → GetMetaFileBitsEx 取位。若完整取回全部位,则对首条 MFCOMMENT 记录的校验和字段做修正(~checksum + 1)。
5.6 播放记录细节补充
META_ESCAPE 的安全过滤(PlayMetaFileRecord 第 843~854 行):播放时对 Escape 记录做白名单式检查------
GETSCALINGFACTOR/GETPHYSPAGESIZE/GETPRINTINGOFFSET属于"查询类",直接返回 FALSE(源码注释:直接解引用会导致空指针崩溃);SETABORTPROC被过滤并打印FIXME("Filtering Escape(SETABORTPROC), possible virus?\n")------因为恶意元文件可以通过 SETABORTPROC 注入回调地址,这是著名的元文件攻击向量之一;- 其余 Escape 才转发给
Escape(hdc, ...)。
META_DIBBITBLT 的两种布局判定 (第 960~982 行):依据注释(Caolan.McNamara@ul.ie),META_DIBBITBLT 有两种变体------带源 DC 时内嵌 DIB,源 DC 为 NULL 时等价于 PatBlt。播放端用 mr->rdSize > 12 区分:大记录按 StretchDIBits 解析(BITMAPINFO 从 rdParm8 开始),小记录退化为 PatBlt。
META_BITBLT / META_STRETCHBLT 的位图重建 (第 900~915 / 882~898 行):这两类旧式记录内嵌的是"设备相关位图"(宽/高/planes/bitspixel + 原始位),播放时先 CreateCompatibleDC(hdc) 建临时 DC,CreateBitmap(width, height, planes, bitspixel, &rdParm[15]) 重建位图并 SelectObject,再 BitBlt/StretchBlt 到目标 DC,最后 DeleteDC 清理。注意参数索引:STRETCHBLT 的位图参数从 rdParm10...14,BITBLT 从 rdParm7...11。
META_TEXTOUT 的字符串定位 (第 696~701 行):ANSI 字符串从 rdParm + 1 开始(第 0 个 WORD 是长度 s1),字符串按 WORD 对齐(((s1+1)>>1)*2),x/y 坐标在字符串之后:x = rdParm[((s1+1)>>1)+2]、y = rdParm[((s1+1)>>1)+1]。
META_CREATEPATTERNBRUSH 的两种子类型 (第 755~779 行):rdParm[0] 为 BS_PATTERN 时用 CreateBitmap + CreatePatternBrush(位图头 BITMAPINFOHEADER 在 rdParm+2,像素在其后);BS_DIBPATTERN 时用 CreateDIBPatternBrushPt(infohdr, rdParm[1])(rdParm[1] 是颜色用法 DIB_RGB_COLORS 等)。
META_CREATEFONTINDIRECT 的位字段拆分 (第 792~811 行):16 位 LOGFONT16 中若干字段按"一 WORD 拆两字节"存放:lfItalic = LOBYTE(rdParm[5])、lfUnderline = HIBYTE(rdParm[5])、lfStrikeOut = LOBYTE(rdParm[6])、lfCharSet = HIBYTE(rdParm[6])、lfOutPrecision = LOBYTE(rdParm[7])、lfClipPrecision = HIBYTE(rdParm[7])、lfQuality = LOBYTE(rdParm[8])、lfPitchAndFamily = HIBYTE(rdParm[8]);lfFaceName 从 rdParm+9 拷贝 LF_FACESIZE 字节。
MF_Play_MetaCreateRegion 的 band 校验 (第 1331~1369 行):解析 META_CREATEREGION 时做三重防御------band 分隔符必须为偶数(*start/2 == (*start+1)/2)、band 结束指针不得越过记录尾(end > (WORD*)mr + mr->rdSize 则失败)、首尾分隔符必须相等;通过后逐对 x 坐标 SetRectRgn + CombineRgn(RGN_OR) 重建区域。
6. 服务器元文件对象的使用场景(剪贴板 / EMF↔WMF)
内核 NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits 的唯一调用方是 gdi32 的剪贴板转换代码,构成"用户态对象 ↔ 内核快照"的双向桥:
6.1 把 EMF 存入内核(GdiConvertEnhMetaFile)
gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 41~67 行:GetEnhMetaFileBits 取 EMF 原始字节 → NtGdiCreateServerMetaFile(GDITAG_TYPE_EMF, nSize, Buffer, 0, 0, 0) 得到内核句柄 hmo(mm/xExt/yExt 传 0,EMF 尺寸在自身头内)。
6.2 从内核取回并重建本地 EMF(GdiCreateLocalEnhMetaFile)
同上文件第 74~120 行:两段式 NtGdiGetServerMetaFileBits 取数据与 iType/mm/xExt/yExt;若 iType == GDITAG_TYPE_MFP(跨剪贴板从 WMF 来)则用 SetWinMetaFileBits(nSize, Buffer, NULL, &Info)(Info 携带 mm/xExt/yExt)转换回 EMF;否则直接 SetEnhMetaFileBits。
6.3 METAFILEPICT:传统 WMF 剪贴板格式(GdiCreateLocalMetaFilePict)
gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 95~194 行:分配 GlobalAlloc(GHND|GMEM_SHARE) 的 METAFILEPICT({mm, xExt, yExt, hMF}),两段式取内核数据;若内核存的是 EMF (iType==GDITAG_TYPE_EMF),需要先 SetEnhMetaFileBits → 读 ENHMETAHEADER 计算 mm=MM_ANISOTROPIC、xExt/yExt = rclFrame 宽高 → CreateDCW(L"DISPLAY") + GetWinMetaFileBits 把 EMF 转成 WMF 字节,再 SetMetaFileBitsEx 得到传统 HMETAFILE 放进 METAFILEPICT。
6.4 把剪贴板 METAFILEPICT 存入内核(GdiConvertMetaFilePict)
同上文件第 201~241 行:GlobalLock 取 METAFILEPICT → GetMetaFileBitsEx 取 WMF 字节 → NtGdiCreateServerMetaFile(GDITAG_TYPE_MFP, nSize, Buffer, pInfo->mm, pInfo->xExt, pInfo->yExt)------把 mm/xExt/yExt 一并保存,供 6.2 反向转换使用。
6.5 剪贴板数据流
进程 A(EMF 程序) 进程 B(WMF 程序)
GetEnhMetaFileBits │
↓ NtGdiCreateServerMetaFile(EMF) │
内核 METATYPEOBJ (iType='XEFM') ──剪贴板──→ NtGdiGetServerMetaFileBits
(GDI_OBJ_HMGR_PUBLIC 公共句柄) ↓
GdiCreateLocalMetaFilePict
→ GetWinMetaFileBits(EMF→WMF)
→ SetMetaFileBitsEx → HMETAFILE
7. 调用链(mermaid)
win32k.sys gdi32.dll 应用程序 win32k.sys gdi32.dll 应用程序 #mermaid-svg-V8QDucQ2ZaTU6EJZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .error-icon{fill:#552222;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .marker.cross{stroke:#333333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ p{margin:0;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V8QDucQ2ZaTU6EJZ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .sequenceNumber{fill:white;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #sequencenumber{fill:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageText{fill:#333;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V8QDucQ2ZaTU6EJZ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .noteText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actorPopupMenu{position:absolute;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-man circle,#mermaid-svg-V8QDucQ2ZaTU6EJZ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 录制(WMF) 播放 剪贴板/EMF 服务器对象 CreateMetaFileW(NULL)MFDRV_AllocMetaFile(OBJ_METADC) + push_dc_driver(MFDRV_Funcs)Rectangle(hdcMF, ...)MFDRV_Rectangle → MFDRV_MetaParam4 → MFDRV_WriteRecordCloseMetaFile(hdcMF)MFDRV_MetaParam0(META_EOF) → MF_Create_HMETAFILEHMETAFILEPlayMetaFile(hdc, hmf)get_metafile_bits → 循环 METARECORDPlayMetaFileRecord → GDI APITRUEGdiConvertEnhMetaFile(hemf)NtGdiCreateServerMetaFile(EMF, data)hmo (META_TYPE, GDI_OBJ_HMGR_PUBLIC)GdiCreateLocalMetaFilePict(hmo)NtGdiGetServerMetaFileBits(hmo, ...)iType/mm/xExt/yExt + 数据HGLOBAL(METAFILEPICT)
#mermaid-svg-4NiX7aORvFvZfYG3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4NiX7aORvFvZfYG3 .error-icon{fill:#552222;}#mermaid-svg-4NiX7aORvFvZfYG3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4NiX7aORvFvZfYG3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .marker.cross{stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4NiX7aORvFvZfYG3 p{margin:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label text{fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label span{color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label span p{background-color:transparent;}#mermaid-svg-4NiX7aORvFvZfYG3 .label text,#mermaid-svg-4NiX7aORvFvZfYG3 span{fill:#333;color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .node rect,#mermaid-svg-4NiX7aORvFvZfYG3 .node circle,#mermaid-svg-4NiX7aORvFvZfYG3 .node ellipse,#mermaid-svg-4NiX7aORvFvZfYG3 .node polygon,#mermaid-svg-4NiX7aORvFvZfYG3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .rough-node .label text,#mermaid-svg-4NiX7aORvFvZfYG3 .node .label text,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label,#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4NiX7aORvFvZfYG3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .rough-node .label,#mermaid-svg-4NiX7aORvFvZfYG3 .node .label,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label,#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label{text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .node.clickable{cursor:pointer;}#mermaid-svg-4NiX7aORvFvZfYG3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .arrowheadPath{fill:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4NiX7aORvFvZfYG3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster text{fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster span{color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4NiX7aORvFvZfYG3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape p,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label rect,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4NiX7aORvFvZfYG3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4NiX7aORvFvZfYG3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MEMORY
DISK
是
否
CreateMetaFileW
MFDRV_AllocMetaFile
push_dc_driver MFDRV_Funcs
GDI 调用 → MFDRV_*
MFDRV_MetaParamN / 专用组装
MFDRV_WriteRecord
mtType
HeapReAlloc + memcpy 追加
WriteFile 追加
更新 mtSize / mtMaxRecord
CloseMetaFile 写 META_EOF
HMETAFILE
PlayMetaFile
get_metafile_bits
循环 METARECORD
rdSize<3 / META_EOF?
恢复 DC + 清理句柄表
PlayMetaFileRecord
8. 与其它分册的关系
| 关联项 | 说明 |
|---|---|
| 《分析_31》2.2 表(其余行)metafile.c | 本文档展开 |
| DC 生命周期 / dclife.c(《分析_49》) | GreCreateCompatibleDC、DC_vInitDc(DCTYPE_INFO)、DC_AllocDcWithHandle |
| GDI 对象管理 / gdiobj.c | GDIObjType_META_TYPE(0x15) 分配/加锁/清理/进程回收 |
| 增强元文件(EMF) | gdi32/wine/emfdc.c(录制 EMR*)、objects/enhmfile.c(转换) |
| LDEV_DEVICE_META / multidisp.c 桩(《分析_25》) | 元文件设备模型的内核侧占位 |
| 剪贴板 | CF_METAFILEPICT、METAFILEPICT、OleSetClipboard 等上层使用 |
9. 源码索引
| 文件 | 关键内容 |
|---|---|
| win32ss/gdi/ntgdi/metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) | METATYPEOBJ;NtGdiCreateMetafileDC / NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits / NtGdiConvertMetafileRect |
| win32ss/gdi/ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c) | GreCreateCompatibleDC(ALT DC + DCTYPE_INFO) |
| win32ss/gdi/ntgdi/dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h) | DCTYPE 枚举、DC/DCLEVEL 结构 |
| win32ss/gdi/ntgdi/gdiobj.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdiobj.c) | GDIOBJ_AllocObjWithHandle / LockObject / vSetObjectOwner / apfnCleanupMETA |
| win32ss/include/ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h) | GDIObjType_META_TYPE(0x15)、GDILoObjType_LO_META_TYPE(0x150000)、GDITAG_TYPE_EMF/'XEFM'、GDITAG_TYPE_MFP/'_PFM' |
| win32ss/gdi/gdi32/wine/mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) | MFDRV_Funcs、MFDRV_AllocMetaFile、CreateMetaFileW/A、CloseMetaFile、MFDRV_WriteRecord、MFDRV_MetaParam0/1/2/4/6/8、MFDRV_ExtEscape、MFDRV_GetDeviceCaps |
| win32ss/gdi/gdi32/wine/mfdrv/graphics.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/graphics.c) | MFDRV_MoveTo/LineTo/Arc/Pie/Chord/Ellipse/Rectangle/RoundRect/SetPixel、MFDRV_MetaPoly、MFDRV_CreateRegion、MFDRV_PaintRgn/InvertRgn/FillRgn/FrameRgn、MFDRV_PolyBezier(桩) |
| win32ss/gdi/gdi32/wine/mfdrv/dc.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/dc.c) | DC 状态类记录(SaveDC/RestoreDC/SetMapMode/SetBkColor/...)、路径类全部返回 FALSE |
| win32ss/gdi/gdi32/wine/mfdrv/objects.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/objects.c) | 句柄表管理、MFDRV_CreateBrushIndirect/CreateFontIndirect/CreatePenIndirect/CreatePalette/SelectPalette/RealizePalette |
| win32ss/gdi/gdi32/wine/mfdrv/bitblt.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/bitblt.c) | MFDRV_PatBlt、MFDRV_StretchBlt(STRETCH_VIA_DIB → META_DIBSTRETCHBLT) |
| win32ss/gdi/gdi32/wine/mfdrv/text.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/text.c) | MFDRV_MetaExtTextOut / MFDRV_ExtTextOut(META_EXTTEXTOUT 布局) |
| win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h) | METAFILEDRV_PDEVICE、全部 MFDRV_* 原型 |
| win32ss/gdi/gdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c) | PlayMetaFile / PlayMetaFileRecord / EnumMetaFile / GetMetaFileBitsEx / SetMetaFileBitsEx / GetWinMetaFileBits / MF_ReadMetaFile / MF_Play_MetaCreateRegion / MF_Play_MetaExtTextOut / METAHEADERDISK |
| win32ss/gdi/gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) | GdiIsPlayMetafileDC / GdiIsMetaFileDC / GdiIsMetaPrintDC、GdiCreateLocalMetaFilePict、GdiConvertMetaFilePict |
| win32ss/gdi/gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) | GdiConvertEnhMetaFile、GdiCreateLocalEnhMetaFile(服务器元文件 ↔ EMF) |
| win32ss/gdi/gdi32/wine/rosglue.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/rosglue.c) | alloc_dc_ptr(OBJ_ENHMETADC) → NtGdiCreateMetafileDC(NULL) + GdiSetLDC |
| win32ss/gdi/gdi32/wine/gdi_private.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/gdi_private.h) | METAFILE_MEMORY(1) / METAFILE_DISK(2) / MFVERSION(0x300) |
| sdk/include/psdk/wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) | METAHEADER、METARECORD、HANDLETABLE、META_* 函数码 |
10. 记录格式字节级示例
为了直观理解"参数逆序 + WORD 单位"的约定,下面手工推导两条典型记录的内存布局。
10.1 META_MOVETO(无对象创建)
假设应用在元文件 DC 上调用 MoveToEx(hdcMF, 100, 50, NULL),gdi32 路由到 MFDRV_MoveTo(dev, x=100, y=50):
c
MFDRV_MetaParam2(dev, META_MOVETO /* 0x0214 */, 100, 50);
MFDRV_MetaParam2 的构造逻辑:rdSize = 5(3 + 2 参数),params[0] = param2 = 50(y),params[1] = param1 = 100(x)。生成的内存字节(小端,WORD 序):
偏移 内容(HEX) 含义
0x00 05 00 00 00 rdSize = 5(共 5 WORD = 10 字节)
0x04 14 02 rdFunction = META_MOVETO (0x0214)
0x06 32 00 rdParm[0] = y = 50 ← 注意:最后一个参数在最前
0x08 64 00 rdParm[1] = x = 100
播放端 PlayMetaFileRecord 的 META_MOVETO 分支按逆序还原:
c
case META_MOVETO:
MoveToEx(hdc, (SHORT)mr->rdParm[1] /* x=100 */, (SHORT)mr->rdParm[0] /* y=50 */, NULL);
break;
10.2 META_EXTTEXTOUT(变长记录示例)
假设 ExtTextOut(hdcMF, x=10, y=20, ETO_OPAQUE, &rect, "A", 1, NULL)("A" 转 ANSI 后 1 字节)。MFDRV_MetaExtTextOut 计算:
len = sizeof(METARECORD) /* 8 */
+ ((count+1)>>1)*2 /* ((1+1)>>1)*2 = 2(字符串按 WORD 对齐) */
+ 2*sizeof(short) /* x,y 两个 SHORT = 4 */
+ sizeof(UINT16) /* flags = 2 */
+ sizeof(RECT16) /* ETO_OPAQUE → 带矩形 = 8 */
= 8+2+4+2+8 = 24 字节 → rdSize = 12
布局:rdParm[0]=y, [1]=x, [2]=count=1, [3]=flags,rdParm[4..7]=RECT16,rdParm[8]=字符串 "A"(不足 WORD 部分由 HeapAlloc 的 ZERO_MEMORY 补齐)。播放端 MF_Play_MetaExtTextOut 通过"mr->rdSize 与 len 比较"判定是否存在 dx 数组(rdSize == len/2 说明没有;rdSize == (len + count*sizeof(INT16))/2 说明有),再调用 ExtTextOutA。
10.3 内存型元文件的整体布局
0x00 METAHEADER(18 字节 = 9 WORD)
mtType=1(MEMORY) mtHeaderSize=9 mtVersion=0x0300 mtSize=总WORD数
mtNoObjects=N mtMaxRecord=最大记录WORD数 mtNoParameters=0
0x12 第 1 条 METARECORD(rdSize/rdFunction/rdParm...)
... 第 2 条、第 3 条 ...
META_EOF 记录(rdSize=3, rdFunction=0)
播放器从 mtHeaderSize*2 = 0x12 开始顺序扫描,直到 offset >= mtSize*2 或遇到 META_EOF。
11. 常见问题与陷阱
11.1 16 位坐标截断
WMF 的所有坐标都是 16 位(POINTS/SHORT)。MFDRV_Polyline/Polygon 把 32 位 POINT 逐元素赋给 POINTS 时静默截断(超出 ±32767 的坐标会回绕)。因此超大坐标的绘图在 WMF 中会失真,这是格式本身的上限,不是 bug。
11.2 为什么磁盘元文件头里 mtType 也是 MEMORY
MFDRV_CloseMetaFile 在回写磁盘文件头时把 mtType 临时改为 METAFILE_MEMORY(init.c 第 427 行注释"This is what windows does")。因此读取侧统一按内存型格式解析 :先读 18 字节头部,校验 mtType==MEMORY,再按 mtSize 读余下全部记录。MF_ReadMetaFile 遇到 DISK 类型的头也会把它纠正为 MEMORY(metafile.c 第 182~185 行)。
11.3 播放的健壮性
PlayMetaFile 对不可信元文件做了两道防护:
mr->rdSize < 3立即终止循环(防止畸形记录把 offset 算到缓冲外);- 播放前保存目标 DC 的画笔/画刷/调色板/剪裁区,结束后全部恢复------即使中间有记录失败,DC 状态也不会被污染。
PlayMetaFileRecord 对未知记录返回 FALSE 并打 WARN,但 PlayMetaFile 主循环不检查返回值(保持 Windows 兼容行为:尽量播放能播放的记录)。
11.4 服务器元文件对象与 HMETAFILE 的区别
| 项 | HMETAFILE(用户态) | 服务器元文件对象(内核) |
|---|---|---|
| 句柄类型 | OBJ_METAFILE(LO_METAFILE_TYPE) | GDIObjType_META_TYPE(LO_META_TYPE) |
| 数据位置 | gdi32 进程堆(METAHEADER 缓冲) | win32k 内核池(METATYPEOBJ 尾部) |
| 生命周期 | 进程内,DeleteMetaFile 释放 | 进程/会话,GDI_CleanupForProcess 回收 |
| 跨进程 | 不直接共享 | 公共句柄(GDI_OBJ_HMGR_PUBLIC)可跨进程 |
| 用途 | 录制/播放/枚举 | 剪贴板桥接(EMF↔WMF 中转) |
两者通过 NtGdiCreateServerMetaFile(本地 → 内核)与 NtGdiGetServerMetaFileBits(内核 → 本地重建)互相转换。
11.5 大小限制
mh->mtSize是 DWORD,按 WORD 计,因此内存型元文件理论上限约 8 GB,但实际受堆扩容策略(size += size/2 + rlen,每次扩 50%+本次记录)影响,大元文件会频繁 realloc;mtNoObjects是 WORD(上限 65535 个对象),MFDRV_AddHandle用UINT16索引,对象数超出会溢出。
12. ReactOS 与 Windows/Wine 的差异要点
- 内核侧更精简 :Windows 的 win32k 有完整的 metafile 内核支持;ReactOS 只保留了"服务器元文件对象"(剪贴板桥)与 ALT DC 底座,录制/播放全部下沉到 gdi32(源自 Wine 的实现),
NtGdiConvertMetafileRect未实现; - CreateMetaFileW 的 DC 底座 :Wine 的 mfdrv DC 不依赖内核 DC 句柄(物理设备链完全在用户态);ReactOS 的 EMF DC(
alloc_dc_ptr(OBJ_ENHMETADC))则额外向内核申请一个 ALT DC(NtGdiCreateMetafileDC)作为句柄底座,再通过GdiSetLDC关联,这是 ReactOS 特有的"用户态对象 + 内核句柄"桥接模式; - EMF↔WMF 转换 :ReactOS 的
GdiConvertEnhMetaFile/GdiConvertMetaFilePict/GdiCreateLocalMetaFilePict/GdiCreateLocalEnhMetaFile以"内核服务器元文件对象"为中转站,与 Wine 纯用户态的内存中转路径不同(Wine 直接传递全局内存句柄); - 剪贴板 :
METAFILEPICT相关的GdiCreateLocalMetaFilePict中 EMF→WMF 使用MM_ANISOTROPIC(源码注释注明 wine 用 MM_ISOTROPIC),属刻意取舍; - 路径/贝塞尔:WMF 不支持路径与贝塞尔,mfdrv 一律返回 FALSE,与 Wine 一致(EMF 才支持这些特性,见 emfdc.c)。
附录 A:GDI32 元文件 API 速查
A.1 录制/管理
| API | 说明 | 实现位置 |
|---|---|---|
CreateMetaFileW/A |
新建元文件 DC(内存型/磁盘型) | mfdrv/init.c |
CloseMetaFile |
结束录制,返回 HMETAFILE(写 META_EOF) | mfdrv/init.c |
DeleteMetaFile |
释放 HMETAFILE 及缓冲 | wine/metafile.c |
CopyMetaFileW/A |
复制元文件(到新内存对象或磁盘文件) | wine/metafile.c |
GetMetaFileW/A |
从磁盘文件读入元文件 | wine/metafile.c |
SetMetaFileBitsEx |
从原始字节创建 HMETAFILE | wine/metafile.c |
GetMetaFileBitsEx |
取 HMETAFILE 原始字节 | wine/metafile.c |
A.2 播放/枚举
| API | 说明 | 实现位置 |
|---|---|---|
PlayMetaFile |
在目标 DC 上重放整个元文件 | wine/metafile.c |
PlayMetaFileRecord |
重放单条 METARECORD(分发 switch) | wine/metafile.c |
EnumMetaFile |
遍历记录并回调 MFENUMPROC | wine/metafile.c |
GetWinMetaFileBits |
EMF → WMF 位数据 | wine/metafile.c |
A.3 服务器元文件(内核桥接)
| API | 说明 | 实现位置 |
|---|---|---|
NtGdiCreateMetafileDC |
创建 ALT DC(EMF DC 底座) | ntgdi/metafile.c |
NtGdiCreateServerMetaFile |
创建内核服务器元文件对象 | ntgdi/metafile.c |
NtGdiGetServerMetaFileBits |
读取内核服务器元文件数据 | ntgdi/metafile.c |
GdiConvertEnhMetaFile |
HENHMETAFILE → 内核对象(EMF) | objects/enhmfile.c |
GdiCreateLocalEnhMetaFile |
内核对象 → HENHMETAFILE | objects/enhmfile.c |
GdiCreateLocalMetaFilePict |
内核对象 → METAFILEPICT(WMF) | objects/metafile.c |
GdiConvertMetaFilePict |
METAFILEPICT → 内核对象(MFP) | objects/metafile.c |
GdiIsPlayMetafileDC |
判断 DC 是否处于元文件播放态 | objects/metafile.c |
GdiIsMetaFileDC |
判断 DC 是否为元文件 DC | objects/metafile.c |
GdiIsMetaPrintDC |
判断 DC 是否为元文件打印 DC | objects/metafile.c |
A.4 对象类型检查
GetObjectType 可用来在运行时区分元文件相关对象:
| GetObjectType 返回值 | 对象 |
|---|---|
| OBJ_METADC | 16 位元文件录制 DC |
| OBJ_METAFILE | HMETAFILE |
| OBJ_ENHMETADC | EMF 录制 DC(ReactOS 上内核底座为 ALT DC) |
| OBJ_ENHMETAFILE | HENHMETAFILE |
附录 B:调试要点
- 串口观察 UNIMPLEMENTED :
NtGdiConvertMetafileRect被调用时串口出现WARNING: ... is UNIMPLEMENTED!; - 调试通道 :mfdrv 与播放代码统一使用
WINE_DEFAULT_DEBUG_CHANNEL(metafile),启用方式为设置调试通道metafile(Wine 风格调试环境);内核侧 metafile.c 使用DPRINT家族; - 录制内容检查 :
GetMetaFileBitsEx(hmf, 0, NULL)得到字节数,再取数据即可用十六进制工具核对 META_* 记录布局(参考第 10 节字节示例); - 畸形元文件防护验证 :构造
rdSize < 3的记录验证播放器安全终止;构造SETABORTPROCEscape 验证过滤逻辑(串口应出现 FIXME 过滤提示); - 句柄泄漏排查 :播放结束应看到句柄表
mtNoObjects个对象全部被DeleteObject;若进程 GDI 句柄持续增长,重点检查PlayMetaFileRecord中MF_AddHandle失败路径。
13. 总结
metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 是 win32k 中一个"小而精"的系统服务文件:
- 4 个入口 :
NtGdiCreateMetafileDC(ALT DC 底座)、NtGdiCreateServerMetaFile(创建内核元文件对象)、NtGdiGetServerMetaFileBits(读取内核元文件数据)、NtGdiConvertMetafileRect(未实现桩); - 1 个私有结构 :
METATYPEOBJ(BASEOBJECT + iType/mm/xExt/yExt/cjData + 尾部数据),数据与结构一体分配; - 核心价值 :为剪贴板提供"跨进程、带标签、整块快照"的元文件数据容器,配合 gdi32 的
GdiConvert*系列完成 EMF↔WMF 双向转换; - 真正的元文件语义在 gdi32 :录制由 mfdrv(MFDRV_* → METARECORD),播放由
PlayMetaFile/PlayMetaFileRecord完成,格式约定为"WORD 单位、参数逆序、16 位坐标"。
理解本文件需要同时把握三条线:内核 GDI 对象机制(gdiobj.c)、用户态元文件物理驱动(mfdrv)、以及播放/转换层(gdi32/wine/metafile.c 与 objects/metafile.c)。
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/metafile.c及关联模块分析(2026 年 8 月)