ReactOS 窗口系统分析(2):窗口类 --- class.c
本文是《ReactOS 窗口系统架构分析》"主线一:窗口显示"系列的第 2 册。
第 1 册(
ReactOS 窗口系统分析_1.md)分析了窗口创建主路径window.c;本册深入剖析"窗口类"------窗口创建之前的那一半故事。所有内容均基于
d:\reactos\win32ss\user\ntuser\class.c(2984 行)的真实源码,函数行号以该文件为准。
目录
1. 概述
窗口类(Window Class)是 Win32 窗口系统"模板复用"的基石:应用程序先用
RegisterClassEx 注册一个类(窗口过程、样式、图标、光标、背景画刷、附加字节等),
然后反复用 CreateWindowEx 按这个模板创建任意多个窗口实例。一个类的全部信息在内核中
凝缩为一个 CLS 对象,挂在进程(或桌面)的类链表上;每个窗口的 WND 对象里则保存一个
指向所属类的指针 pcls。
整个"类"生命周期的主线如下:
用户模式 (user32.dll) 内核模式 (win32k.sys)
┌─────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
│ RegisterClassExW ──► RegisterClassExWOWW │ │ NtUserRegisterClassExWOW │
│ │ (转换 ANSI/菜单名/版本化类名) │──►│ ├─ Probe 参数(SEH) │
│ │ │ │ ├─ UserRegisterClass │
│ │ │ │ │ ├─ IntCreateClass ──► 分配 CLS 对象 │
│ │ │ │ │ └─ 挂入 pclsPrivateList / pclsPublicList │
│ ▼ │ │ ▼ │
│ CreateWindowExW │ │ NtUserCreateWindowEx │
│ │ │──►│ └─ co_UserCreateWindowEx │
│ │ │ │ └─ IntCreateWindow (window.c) │
│ │ │ │ ├─ IntGetAndReferenceClass(找类) │
│ │ │ │ ├─ UserCreateObject 分配 WND │
│ │ │ │ ├─ pWnd->pcls = Class(挂接) │
│ │ │ │ └─ pWnd->lpfnWndProc = 类的窗口过程 │
│ ▼ │ │ ▼ │
│ UnregisterClassW ──► NtUserUnregisterClass│──►│ UserUnregisterClass(解链、销毁 CLS) │
└─────────────────────────────────────────┘ └──────────────────────────────────────────────┘
本册覆盖的文件:
| 文件 | 角色 |
|---|---|
win32ss/user/ntuser/class.c |
窗口类的内核实现(本册主角,2984 行) |
win32ss/user/ntuser/class.h |
类的内部接口声明(IntGetAndReferenceClass 等) |
win32ss/include/ntuser.h |
CLS 结构、ICLS_* 索引、CSF_* 标志、FNID_* 定义 |
win32ss/user/user32/windows/class.c |
用户态 RegisterClassExW/GetClassInfo/SetClassLong 等封装 |
win32ss/user/user32/controls/regcontrol.c |
用户态内置控件类注册(RegisterSystemControls) |
win32ss/user/ntuser/callproc.c |
窗口过程调用转发对象 CALLPROCDATA(A/W 转换) |
win32ss/user/ntuser/ntuser.c |
RegisterControlAtoms 调用点、InitUserAtoms 系统类原子 |
win32ss/user/ntuser/window.c |
窗口创建时对类的消费(IntCreateWindow) |
2. 设计动机
2.1 模板复用:类与实例分离
如果没有"类",每个窗口都要重复提供窗口过程、图标、光标、背景画刷、样式等一整套属性。
Windows 将这套属性抽成"类"(class),窗口创建时从类拷贝默认值,运行期间又可通过
SetClassLong/GetClassLong 在类层面统一修改所有该类的窗口(例如 SetClassLong(hWnd, GCL_HICON, ...)
会改变该类所有窗口的图标)。这就是"类 = 模板,窗口 = 实例"的模型,对应到内核正是:
- 模板 :
CLS对象(class.c 管理) - 实例 :
WND对象(window.c 管理),其pcls字段指向模板
2.2 系统类与应用类
class.c 把类划分为两个世界:
- 系统类(Server/System Classes) :由
win32k.sys内建(UserRegisterSystemClasses)或由
user32.dll 注册(RegisterSystemControls),如#32770(对话框)、#32769(桌面)、ScrollBar、
Button、Edit、Static、ListBox、ComboBox、Message等。系统类进程不可卸载(UnregisterClass
拒绝或忽略),且常带CS_GLOBALCLASS或CSF_SERVERSIDEPROC标志。 - 应用类(App Classes) :应用程序自己注册的类,挂在进程的私有链表
pclsPrivateList
(无CS_GLOBALCLASS)或公共链表pclsPublicList(有CS_GLOBALCLASS,可被其他模块按
"模块+类名" 查找)。
2.3 原子(Atom)作为类名
类名在系统内不是字符串比较,而是先经原子表(gAtomTable)转成 16 位原子(RTL_ATOM),
再按原子数值比较。class.c 维护两个原子:
atomClassName:版本化类名(激活上下文重定向后的类名)原子,实际匹配用;atomNVClassName:非版本化类名 (用户注册时传入的原始类名)原子,GCW_ATOM返回它。
这套"双原子"设计配合 user32 的 ClassNameToVersion(激活上下文窗口类重定向),使 comctl32
等版本化控件类可以按 manifest 重定向。
2.4 桌面隔离与克隆
CLS 对象分配在"桌面堆"(desktop heap)中,rpdeskParent 记录所属桌面。同一个类若被
不同桌面上的窗口使用,会被克隆 (pclsClone 链表):基类 pclsBase + 若干克隆。窗口引用计数
cWndReferenceCount 决定类何时真正可销毁。这是 ReactOS 窗口类与 Windows 最不同的实现细节之一,
也是本册"类查找"与"引用计数"两章的重点。
2.5 兼容性细节(wine/apitest 驱动的行为)
class.c 中多处注释直接引用 wine 测试或 user32_apitest,这些"看似奇怪"的行为都是
兼容性要求,列举如下:
GetClassInfo返回类原子 :Windows 未文档化的行为,NtUserGetClassInfo在成功时把
类原子当作 BOOL 返回(L2861-L2863,注释引用 Wine:"We must return the atom of the class
here instead of just TRUE")。- 系统类
GetClassInfo的 style 清掉CS_GLOBALCLASS:UserGetClassInfo中
if (Class->fnid) lpwcx->style &= ~CS_GLOBALCLASS;(L2290-L2291),对应 wine class test
的check_style。 WC_SWITCH/WC_MENU的附加字节 :DefaultServerClasses注释分别写明
"See user32_apitest GetClassInfo, 0: Pointer to ALTTABINFO"(sizeof(LONG_PTR))与
"See user32_apitest GetClassInfo, PopupMenuWndProcW"(16 字节)。- wine "deftest" :
IntSetClassWndProc/IntCreateClass注释反复引用 deftest------
窗口过程 A/W 类型要自动检测,不能靠注册标志硬编码。 - 原子不残留 :
IntDestroyClass注销atomClassName/atomNVClassName的注释
"Fixes running the static test then run class test issue"------某些应用退出前不调
UnregisterClass,若不删原子会导致大小写不敏感比较时原子复用出错。 - Bug 4778 :
IntGetClassForDesktop克隆类时若默认控件类的hModule不是hModClient
则重置(否则控件类跨桌面克隆后模块归属错误)。 - Edit 控件预建 CallProc :window.c L2035-L2049 注释引用实测(Win2k/XP 行为)------
创建 Edit 控件窗口时立即为其建一个CALLPROCDATA。
3. 核心数据结构
3.1 CLS ------ 窗口类对象(ntuser.h L565-L596)
c
typedef struct _CLS
{
struct _CLS *pclsNext; // 类链表(同一进程 pclsPrivateList/pclsPublicList)下一项
RTL_ATOM atomClassName; // 版本化类名原子(实际查找匹配用)
ATOM atomNVClassName; // 非版本化类名原子(GCW_ATOM 返回)
DWORD fnid; // 窗口功能 ID(FNID_*,如 FNID_BUTTON)
struct _DESKTOP *rpdeskParent; // 所属桌面(NULL 表示在共享堆上)
PVOID pdce; // 类 DC(CS_CLASSDC 时,指向 DCE)
DWORD CSF_flags; // 类状态标志(CSF_*,见 3.5)
PSTR lpszClientAnsiMenuName; // 菜单名 ANSI 副本(给用户态)
PWSTR lpszClientUnicodeMenuName; // 菜单名 Unicode 副本(给用户态)
PCALLPROCDATA spcpdFirst; // 类 CallProc 链表头(A/W 转换句柄)
struct _CLS *pclsBase; // 基类指针(克隆机制:指向自身 = 基类)
struct _CLS *pclsClone; // 克隆链表头(其他桌面的副本)
ULONG cWndReferenceCount; // 该类被多少个窗口引用
UINT style; // CS_* 类样式
WNDPROC lpfnWndProc; // 类窗口过程(内核侧值)
INT cbclsExtra; // 类附加字节数(紧随 CLS 结构之后)
INT cbwndExtra; // 窗口附加字节数(每个 WND 实例多分配)
HINSTANCE hModule; // 注册模块
struct _CURICON_OBJECT *spicn; // 大图标(光标/图标对象)
struct _CURICON_OBJECT *spcur; // 光标
HBRUSH hbrBackground; // 背景画刷
PWSTR lpszMenuName; /* kernel use */
PSTR lpszAnsiClassName; /* kernel use */
struct _CURICON_OBJECT *spicnSm; // 小图标
////
UINT Unicode:1; // !CSF_ANSIPROC
UINT Global:1; // CS_GLOBALCLASS 或 CSF_SERVERSIDEPROC
UINT MenuNameIsString:1; // 菜单名是字符串(需释放)而非资源 ID
UINT NotUsed:29;
} CLS, *PCLS;
要点:
cbClsExtra附加字节 紧跟在CLS结构之后:IntCreateClass以sizeof(*Class) + lpwcx->cbClsExtra
分配;SetClassLong(Index≥0)通过IntGetSetClassLongPtr直接读写(&Class[1]) + Index。cbWndExtra附加字节 在窗口侧:IntCreateWindow以sizeof(WND) + Class->cbwndExtra分配
WND(window.c L1868),每个窗口实例独享。pclsBase/pclsClone构成"基类 + 克隆"结构。只有基类持有spcpdFirst(CallProc 链)、
图标/光标引用等共享资源;克隆只是同一数据的桌面堆副本。- 图标/光标以对象引用 (
spicn/spcur/spicnSm为CURICON_OBJECT*)而非句柄保存,
保证在窗口类存活期间图标对象不被释放;SetClassLong更换图标时按引用计数增减。
3.2 ICLS_* ------ 系统类索引(ntuser.h L912-L943)
系统类通过固定下标在 gpsi->atomSysClass[] 中登记其原子,索引定义:
c
#define ICLS_BUTTON 0
#define ICLS_EDIT 1
#define ICLS_STATIC 2
#define ICLS_LISTBOX 3
#define ICLS_SCROLLBAR 4
#define ICLS_COMBOBOX 5
#define ICLS_MDICLIENT 6
#define ICLS_COMBOLBOX 7
#define ICLS_DDEMLEVENT 8
#define ICLS_DDEMLMOTHER 9
#define ICLS_DDEML16BIT 10
#define ICLS_DDEMLCLIENTA 11
#define ICLS_DDEMLCLIENTW 12
#define ICLS_DDEMLSERVERA 13
#define ICLS_DDEMLSERVERW 14
#define ICLS_IME 15
#define ICLS_GHOST 16
#define ICLS_DESKTOP 17
#define ICLS_DIALOG 18
#define ICLS_MENU 19
#define ICLS_SWITCH 20
#define ICLS_ICONTITLE 21
#define ICLS_TOOLTIPS 22
#define ICLS_UNKNOWN 22
#define ICLS_NOTUSED 23
#define ICLS_SYSSHADOW 23
#define ICLS_HWNDMESSAGE 24
#define ICLS_NOTUSED 25
#define ICLS_END 31
前 17 个(ICLS_BUTTON~ICLS_GHOST)对应 ControlsList[] 中 17 个标准控件名,
由 RegisterControlAtoms(class.c L400)注册;atomSysClass[ICLS_MENU] 起为系统内建类。
atomSysClass 的三种来源:
InitUserAtoms(ntuser.c L34-L48):启动时直接给固定原子号------
ICLS_MENU=32768、ICLS_DESKTOP=32769、ICLS_DIALOG=32770(即#32770)、
ICLS_SWITCH=32771、ICLS_ICONTITLE=32772、ICLS_TOOLTIPS=32774、
ICLS_HWNDMESSAGE= 全局原子"Message"。RegisterControlAtoms(class.c L400-L418):把ControlsList[]的名字逐个加入原子表,
填入atomSysClass[0..16]。IntCreateClass(class.c L1117-L1120):若创建的系统类对应的atomSysClass[iCls]仍为 0,
则用类的版本化原子补填(例如 user32 的RegisterSystemControls注册Button类时)。
3.3 CS_* ------ 类样式(sdk/include/psdk/winuser.h L654-L668)
| 常量 | 值 | 含义 |
|---|---|---|
CS_VREDRAW |
0x0001 | 垂直尺寸变化时重绘整个窗口 |
CS_HREDRAW |
0x0002 | 水平尺寸变化时重绘整个窗口 |
CS_DBLCLKS |
0x0008 | 允许双击消息(否则双击合成两次单击) |
CS_OWNDC |
0x0020 | 每个窗口拥有独立 DC(window.c 中分配 DCE) |
CS_CLASSDC |
0x0040 | 类共享一个 DC(存入 Class->pdce) |
CS_PARENTDC |
0x0080 | 使用父窗口 DC 的裁剪(ScrollBar 类用它) |
CS_NOCLOSE |
0x0200 | 禁用系统菜单"关闭" |
CS_SAVEBITS |
0x0800 | 被遮挡时保存屏幕位图(菜单类用它) |
CS_BYTEALIGNCLIENT |
0x1000 | 客户区按字节对齐 |
CS_BYTEALIGNWINDOW |
0x2000 | 窗口按字节对齐 |
CS_GLOBALCLASS |
0x4000 | 全局类(入 pclsPublicList,跨模块可查) |
CS_IME |
0x10000 | 输入法类 |
CS_DROPSHADOW |
0x20000 | 窗口带投影(菜单类用它) |
CS_GLOBALCLASS 决定类进私有链表还是公共链表:IntCreateClass 中
if (Class->style & CS_GLOBALCLASS) Class->Global = TRUE;(class.c L1218),
UserRegisterClass 再据此选择链表(class.c L1571-L1574)。
3.4 CSF_* ------ 类内部标志(ntuser.h L556-L563)
c
#define CSF_SERVERSIDEPROC 0x0001 // 类窗口过程是 win32k 服务端过程(走 KeUserModeCallback)
#define CSF_ANSIPROC 0x0002 // 类窗口过程是 ANSI 版
#define CSF_WOWDEFERDESTROY 0x0004 // WOW 延迟销毁(IntFindClass 跳过此类)
#define CSF_SYSTEMCLASS 0x0008 // 系统类
#define CSF_WOWCLASS 0x0010 // WOW 类
#define CSF_WOWEXTRA 0x0020 // WOW 附加字节
#define CSF_CACHEDSMICON 0x0040 // 小图标是从大图标缓存生成的(类拥有它,需销毁)
#define CSF_WIN40COMPAT 0x0080 // Win4.0 兼容
其中 CSF_SERVERSIDEPROC 是本文件最常打交道的标志:系统内建类的窗口过程登记在内核
(SERVERINFO.mpFnidPfn,宏 GETPFNSERVER(i)),用户态拿到的则是 apfnClientA/W
(GETPFNCLIENTA/W(i))对应的 user32 过程。类注册时若检测到用户提供的 lpfnWndProc
恰好等于某个 GETPFNCLIENTA/W(i),会自动把 lpfnWndProc 升级为 GETPFNSERVER(i) 并置位
CSF_SERVERSIDEPROC(见 6.3)。
3.5 FNID_* ------ 窗口功能 ID(ntuser.h L857-L899)
CLS.fnid(以及窗口 WND.fnid)用固定数值标记"这个窗口是干什么的":
c
#define FNID_FIRST 0x029A
#define FNID_SCROLLBAR 0x029A
#define FNID_ICONTITLE 0x029B
#define FNID_MENU 0x029C
#define FNID_DESKTOP 0x029D
#define FNID_DEFWINDOWPROC 0x029E
#define FNID_MESSAGEWND 0x029F
#define FNID_SWITCH 0x02A0
#define FNID_BUTTON 0x02A1
#define FNID_COMBOBOX 0x02A2
#define FNID_COMBOLBOX 0x02A3
#define FNID_DIALOG 0x02A4
#define FNID_EDIT 0x02A5
#define FNID_LISTBOX 0x02A6
#define FNID_MDICLIENT 0x02A7
#define FNID_STATIC 0x02A8
#define FNID_IME 0x02A9
#define FNID_GHOST 0x02AA
#define FNID_TOOLTIPS 0x02B6
#define FNID_DDEML 0x2000
#define FNID_DESTROY 0x4000
fnid 的作用贯穿窗口系统:DefWindowProc 按 fnid 分派默认行为;NtUserGetClassName(Real=TRUE)
按 fnid 反查系统类名;LookupFnIdToiCls 把 fnid 映射回 ICLS_* 索引(见 9.1)。
3.6 WNDCLASS / WNDCLASSEX ------ 用户态接口结构
WNDCLASS(WNDCLASSEX 是其扩展版)是 RegisterClass/RegisterClassEx 的入参:
c
typedef struct tagWNDCLASSEXW {
UINT cbSize; // sizeof(WNDCLASSEXW),NtUserRegisterClassExWOW 强制校验
UINT style; // CS_* 类样式
WNDPROC lpfnWndProc; // 窗口过程
int cbClsExtra; // 类附加字节
int cbWndExtra; // 窗口附加字节
HINSTANCE hInstance; // 注册模块(不能为 NULL)
HICON hIcon; // 大图标
HCURSOR hCursor; // 光标
HBRUSH hbrBackground; // 背景画刷
LPCWSTR lpszMenuName; // 菜单名(字符串或 MAKEINTRESOURCE)
LPCWSTR lpszClassName; // 类名(字符串或 MAKEINTATOM)
HICON hIconSm; // 小图标
} WNDCLASSEXW;
3.7 REGISTER_SYSCLASS / CLSMENUNAME / CALLPROCDATA
c
// ntuser.h L482-L494 ------ 系统类注册表项
typedef struct _REGISTER_SYSCLASS
{
PWSTR ClassName; // 类名
UINT Style; // CS_* 样式
WNDPROC ProcW; // 自定义服务端过程(NULL 则用 GETPFNSERVER(fiId))
UINT ExtraBytes; // cbWndExtra
HICON hCursor; // 光标(OCR_NORMAL 表示系统箭头)
HBRUSH hBrush; // 背景画刷(<= COLOR_MENUBAR 时按系统色取)
WORD fiId; // FNID_*
WORD iCls; // ICLS_*
} REGISTER_SYSCLASS;
// ntuser.h L496-L501 ------ 类菜单名(跨内核/用户态传递)
typedef struct _CLSMENUNAME
{
LPSTR pszClientAnsiMenuName; // ANSI 菜单名
LPWSTR pwszClientUnicodeMenuName; // Unicode 菜单名
PUNICODE_STRING pusMenuName; // 指向用户态 UNICODE_STRING
} CLSMENUNAME, *PCLSMENUNAME;
// ntuser.h L548-L554 ------ 窗口过程转发对象
typedef struct _CALLPROCDATA
{
PROCDESKHEAD head; // 对象头(含桌面、引用计数)
struct _CALLPROCDATA *spcpdNext; // 链表(挂在 CLS.spcpdFirst 或 WND 上)
WNDPROC pfnClientPrevious; // 原始客户过程地址
GETCPD wType; // 转换类型(A2U / U2A / Class / Window ...)
} CALLPROCDATA, *PCALLPROCDATA;
3.8 GETPFN* 宏与 SERVERINFO 函数表
CSF_SERVERSIDEPROC 机制的底座是 SERVERINFO(ntuser.h L1049-L1073)里三张函数指针表:
c
typedef struct tagSERVERINFO
{
...
PFN_FNID mpFnidPfn[FNID_NUM]; // 服务端过程表(内核侧,按 FNID 索引)
WNDPROC aStoCidPfn[FNID_NUMSERVERPROC]; // 服务端→客户映射表
USHORT mpFnid_serverCBWndProc[FNID_NUM];
PFNCLIENT apfnClientA; // 用户态 ANSI 过程表(user32 通过
PFNCLIENT apfnClientW; // NtUserInitializeClientPfnArrays 登记)
PFNCLIENTWORKER apfnClientWorker;
...
ATOM atomSysClass[ICLS_NOTUSED+1]; // 系统类原子表(32 项)
...
} SERVERINFO, *PSERVERINFO;
对应的宏(在 win32k 头中定义):
GETPFNSERVER(fnid)→gpsi->mpFnidPfn[fnid - FNID_FIRST]:取内核侧窗口过程,
例如FNID_DESKTOP对应桌面窗口过程、FNID_SCROLLBAR对应滚动条过程;GETPFNCLIENTA(fnid)/GETPFNCLIENTW(fnid)→gpsi->apfnClientA->pfnXxxWndProc:
取 user32 侧 ANSI/Unicode 过程(REGISTER_SYSCLASS的fiId与PFNCLIENT成员
一一对应,见 regcontrol.cRegisterClientPFN的赋值:pfnClientA.pfnButtonWndProc = ButtonWndProcA等)。
class.c 对这三张表的用法:
| 场景 | 代码 | 方向 |
|---|---|---|
| 系统类注册 | wc.lpfnWndProc = GETPFNSERVER(fiId) |
内核取服务端过程 |
| 类创建时升级 | GETPFNCLIENTW(i) == lpwcx->lpfnWndProc → 换 GETPFNSERVER(i) |
客户→服务端 |
| GetClassInfo 还原 | GETPFNSERVER(i) == Class->lpfnWndProc → 返回 GETPFNCLIENTA/W(i) |
服务端→客户 |
| 窗口创建 A/W 校正 | GETPFNCLIENTA(fnid) == pWnd->lpfnWndProc → 换 W 版 |
客户 A↔W |
FNID_NUMSERVERPROC = FNID_SWITCH - FNID_FIRST + 1,即只有 FNID_SCROLLBAR~FNID_SWITCH
这 7 个 fnid 有服务端过程表项;FNID_BUTTON 之后的控件类(用户态注册)没有直接的服务端
过程------它们在注册时被"匹配升级",但 IntGetClassWndProc 仍需通过 fnid 区间
(FNID_BUTTON~FNID_GHOST)从客户表恢复 A/W 双版本。
4. 系统类注册
系统类注册分三层:原子预注册 (RegisterControlAtoms + InitUserAtoms)→ 内核系统类
(UserRegisterSystemClasses,class.c L2335)→ 用户态控件类 (user32 RegisterSystemControls)。
4.1 RegisterControlAtoms(class.c L400-L418)
c
BOOL FASTCALL RegisterControlAtoms(VOID)
{
RTL_ATOM Atom;
UNICODE_STRING ClassName;
INT i = 0;
while ( i < ICLS_DESKTOP) // 只注册前 17 个(Button..Ghost)
{
RtlInitUnicodeString(&ClassName, ControlsList[i]);
if (IntRegisterClassAtom(&ClassName, &Atom))
gpsi->atomSysClass[i] = Atom;
i++;
}
return TRUE;
}
ControlsList[](class.c L14-L33)按序对应 ICLS_* 0~16:
| 索引 | ICLS | 名字 |
|---|---|---|
| 0 | ICLS_BUTTON | Button |
| 1 | ICLS_EDIT | Edit |
| 2 | ICLS_STATIC | Static |
| 3 | ICLS_LISTBOX | ListBox |
| 4 | ICLS_SCROLLBAR | ScrollBar |
| 5 | ICLS_COMBOBOX | ComboBox |
| 6 | ICLS_MDICLIENT | MDIClient |
| 7 | ICLS_COMBOLBOX | ComboLBox |
| 8 | ICLS_DDEMLEVENT | DDEMLEvent |
| 9 | ICLS_DDEMLMOTHER | DDEMLMom |
| 10 | ICLS_DDEML16BIT | DMGClass |
| 11 | ICLS_DDEMLCLIENTA | DDEMLAnsiClient |
| 12 | ICLS_DDEMLCLIENTW | DDEMLUnicodeClient |
| 13 | ICLS_DDEMLSERVERA | DDEMLAnsiServer |
| 14 | ICLS_DDEMLSERVERW | DDEMLUnicodeServer |
| 15 | ICLS_IME | IME |
| 16 | ICLS_GHOST | Ghost |
调用点在 ntuser.c InitUserAtoms()(L34-L48),该函数随后固定填充高编号系统类原子。
注意 ScrollBar 类名注册为字符串 (注册在 atomSysClass[ICLS_SCROLLBAR]),而
NtUserGetClassName(Real=TRUE) 对滚动条窗口返回的正是这个原子对应的名字。
4.2 UserRegisterSystemClasses(class.c L2335-L2438)
c
BOOL FASTCALL UserRegisterSystemClasses(VOID)
{
...
if (ppi->W32PF_flags & W32PF_CLASSESREGISTERED) return TRUE; // 已注册
if (hModClient == NULL) return FALSE; // user32 未加载
for (i = 0; i != ARRAYSIZE(DefaultServerClasses); i++)
{
// 组一个 WNDCLASSEXW:
Flags |= CSF_SERVERSIDEPROC; // 一律服务端过程
if (DefaultServerClasses[i].ProcW) { wc.lpfnWndProc = ProcW; wc.hInstance = hModuleWin; }
else { wc.lpfnWndProc = GETPFNSERVER(fiId); wc.hInstance = hModClient; }
// OCR_NORMAL 光标 → SYSTEMCUR(ARROW);hBrush<=COLOR_MENUBAR → IntGetSysColorBrush
Class = IntCreateClass(&wc, &ClassName, &ClassName, &MenuName,
fiId, Flags, NULL, ppi); // Desktop=NULL → 共享堆
Class->pclsNext = ppi->pclsPublicList; // 挂公共链表
ppi->dwRegisteredClasses |= ICLASS_TO_MASK(iCls); // 登记掩码
}
if (Ret) ppi->W32PF_flags |= W32PF_CLASSESREGISTERED;
return Ret;
}
特征:
- 惰性触发 :
IntGetAndReferenceClass(L1461)、NtUserRegisterClassExWOW(L2480)、
NtUserGetClassInfo(L2828)在发现进程的W32PF_CLASSESREGISTERED未置位时都会先调它。 hModClient == NULL返回 FALSE :hModClient(user32 在内核中的映射基址,ntuser.c L25)
未初始化说明 user32 尚未完成NtUserInitializeClientPfnArrays,此时不能注册。- Desktop 传 NULL :系统类先分配在共享堆 上(
IntCreateClass的Desktop==NULL分支,
L1096-L1102),等第一个窗口在某个桌面创建时再克隆到该桌面堆(见 5.4)。 IFLAGS置CSF_SERVERSIDEPROC:类的lpfnWndProc指向内核侧GETPFNSERVER(fiId)
(如桌面过程、菜单过程、滚动条过程),用户态GetClassInfo通过IntGetClassWndProc
反向映射回 user32 的GETPFNCLIENTA/W(fiId)。- 进程退出清理 :线程销毁时(main.c L816-L823)若进程已无其他线程,先
DestroyProcessClasses
再清除W32PF_CLASSESREGISTERED,保证下次进程(线程池复用场景)可重新注册。
4.3 DefaultServerClasses ------ 内建系统类清单(class.c L37-L104)
| 类名 | 样式 | cbWndExtra | 光标 | 背景 | FNID | ICLS |
|---|---|---|---|---|---|---|
WC_DESKTOP(#32769) |
`CS_GLOBALCLASS | CS_DBLCLKS` | sizeof(ULONG)*2 |
OCR_NORMAL | COLOR_BACKGROUND |
FNID_DESKTOP |
WC_SWITCH(#32771) |
`CS_VREDRAW | CS_HREDRAW | CS_SAVEBITS` | sizeof(LONG_PTR) |
OCR_NORMAL | NULL |
WC_MENU |
`CS_DBLCLKS | CS_SAVEBITS | CS_DROPSHADOW` | 16 | OCR_NORMAL | COLOR_MENU+1 |
ScrollBar |
`CS_DBLCLKS | CS_VREDRAW | CS_HREDRAW | CS_PARENTDC` | sizeof(SBWND)-sizeof(WND) |
OCR_NORMAL |
(Tooltips,#if 0 禁用) |
--- | --- | --- | --- | FNID_TOOLTIPS | ICLS_TOOLTIPS |
WC_ICONTITLE(#32772) |
0 | 0 | OCR_NORMAL | 0 | FNID_ICONTITLE | ICLS_ICONTITLE |
Message |
CS_GLOBALCLASS |
0 | OCR_NORMAL | NULL | FNID_MESSAGEWND | ICLS_HWNDMESSAGE |
说明:
WC_*宏(#32769等)即InitUserAtoms预登记的固定原子,字符串形式就是#32769。ScrollBar的附加字节是SBWND结构在WND之后的部分------滚动条窗口把滚动条状态直接
存在窗口附加字节里。- 菜单类
WC_MENU的 16 字节附加字节对应user32_apitest中PopupMenuWndProcW的期望。 - 对话框类 不在此表 :
#32770由 user32 的RegisterSystemControls注册(见 4.4),
因为对话框过程(DefDlgProc)在 user32 中。
4.4 user32 侧 RegisterSystemControls(regcontrol.c L46-L110)
c
static const struct {
const struct builtin_class_descr *desc;
WORD fnid; WORD ClsId;
} g_SysClasses[] = {
{ &DIALOG_builtin_class, FNID_DIALOG, ICLS_DIALOG}, // "#32770"
{ &COMBO_builtin_class, FNID_COMBOBOX, ICLS_COMBOBOX}, // "ComboBox"
{ &COMBOLBOX_builtin_class, FNID_COMBOLBOX, ICLS_COMBOLBOX}, // "ComboLBox"
{ &MDICLIENT_builtin_class, FNID_MDICLIENT, ICLS_MDICLIENT},
{ &BUTTON_builtin_class, FNID_BUTTON, ICLS_BUTTON},
{ &LISTBOX_builtin_class, FNID_LISTBOX, ICLS_LISTBOX},
{ &EDIT_builtin_class, FNID_EDIT, ICLS_EDIT},
{ &STATIC_builtin_class, FNID_STATIC, ICLS_STATIC},
{ &GHOST_builtin_class, FNID_GHOST, ICLS_GHOST},
{ &IME_builtin_class, FNID_IME, ICLS_IME},
};
每个控件类以 style | CS_GLOBALCLASS、用户态 procW(如 ButtonWndProcW)、cbWndExtra
调 RegisterClassExWOWW(..., fnid, 0, FALSE) 注册。注册成功后在 RegisterDefaultClasses 掩码
中置位(RegisterDefaultClasses |= ICLASS_TO_MASK(ClsId)),并跳过已在 win32k 注册的类
(代码注释 // moved to win32k:MENU、SCROLLBAR、ICONTITLE、POPUPMENU)。RegisterDefaultClasses
本身又是 user32 的惰性标志:GetClassInfoExW/RegisterClassExWOWW 在 !RegisterDefaultClasses
时先 RegisterSystemControls()。
由此构成完整系统类注册矩阵:
win32k(UserRegisterSystemClasses) user32(RegisterSystemControls)
Desktop/#32769、Switch/#32771、Menu、 Dialog/#32770、ComboBox、ComboLBox、
ScrollBar、IconTitle/#32772、Message MDIClient、Button、ListBox、Edit、
Static、Ghost、IME
(服务端过程 CSF_SERVERSIDEPROC) (客户过程,注册时自动匹配升级为服务端)
注意 #32770 与 #32769 的区别:前者(对话框)在 user32 注册、hInstance 是 user32;
后者(桌面)在 win32k 注册、hInstance 是 hModClient(UserRegisterSystemClasses 中
wc.hInstance = hModClient,当 ProcW==NULL 时)。
4.5 系统类注册时机总表
| 注册者 | 函数 | 时机 | 注册内容 | 标志 |
|---|---|---|---|---|
| win32k 启动 | InitUserAtoms(ntuser.c L34) |
InitUserImpl 早期 |
系统类固定原子号 + Message 原子 |
--- |
| win32k 启动 | RegisterControlAtoms(class.c L400) |
InitUserAtoms 内 |
17 个控件名原子(Button...Ghost) | --- |
| 首次需要时 | UserRegisterSystemClasses(class.c L2335) |
每个进程首次找类/注册类/GetClassInfo | Desktop、Switch、Menu、ScrollBar、IconTitle、Message | CSF_SERVERSIDEPROC,挂公共链表 |
| 首次需要时 | RegisterSystemControls(regcontrol.c L71) |
user32 首次 RegisterClassEx/GetClassInfo |
Dialog(#32770)、ComboBox、ComboLBox、MDIClient、Button、ListBox、Edit、Static、Ghost、IME | CS_GLOBALCLASS,经 NtUserRegisterClassExWOW |
| 进程退出 | DestroyProcessClasses(class.c L305) |
进程最后一个线程销毁(main.c L821) | 释放该进程所有私有/公共类 | --- |
| 桌面销毁 | IntCheckProcessDesktopClasses(class.c L1016) |
桌面堆回收 | 把桌面堆上的类迁回共享堆或销毁 | --- |
惰性注册的双保险:IntGetAndReferenceClass(L1461)、NtUserRegisterClassExWOW(L2480)、
NtUserGetClassInfo(L2828)三处入口都会先检查 W32PF_CLASSESREGISTERED 再决定是否调用
UserRegisterSystemClasses;user32 侧 RegisterClassExWOWW/GetClassInfoExW 则检查
RegisterDefaultClasses 再决定是否调用 RegisterSystemControls。两条链互相配合,保证
"系统类在任何窗口操作前就绪"且"每个进程只注册一次"。
5. 类查找与引用
5.1 IntFindClass ------ 链表内精确匹配(class.c L1265-L1292)
c
static PCLS IntFindClass(IN RTL_ATOM Atom, IN HINSTANCE hInstance,
IN PCLS *ClassList, OUT PCLS **Link OPTIONAL)
{
PCLS Class, *PrevLink = ClassList;
Class = *PrevLink;
while (Class != NULL)
{
if (Class->atomClassName == Atom &&
(hInstance == NULL || Class->hModule == hInstance) &&
!(Class->CSF_flags & CSF_WOWDEFERDESTROY))
{
if (Link != NULL) *Link = PrevLink; // 返回前驱指针(注销时用)
break;
}
PrevLink = &Class->pclsNext;
Class = Class->pclsNext;
}
return Class;
}
匹配三条件:版本化类原子相等 + 模块匹配(hInstance==NULL 表示忽略模块,用于全局类)
- 未被 CSF_WOWDEFERDESTROY 标记。返回时可顺带给出链表前驱地址,供注销时解链。
5.2 IntGetAtomFromStringOrAtom(class.c L1294-L1374)
把用户态传入的"类名"统一换算成原子:
ClassName->Length != 0:字符串 → 拷贝到本地缓冲(可能跨 64 字符则池分配)→
RtlLookupAtomInAtomTable(gAtomTable, AtomName, Atom);找不到返回 FALSE(并置
ERROR_CLASS_DOES_NOT_EXIST)。Length == 0且Buffer非空:IS_ATOM形式(0x0001~0xFFFF 的 MAKEINTATOM)------
直接取(RTL_ATOM)(ULONG_PTR)Buffer,无需查表。
该函数要求调用者用 SEH 保护(RtlCopyMemory 会访问用户缓冲)。
5.3 IntGetClassAtom ------ 四步查找(class.c L1376-L1447)
c
/* Step 1: 私有链表中按 (原子, hInstance) 精确匹配 */
Class = IntFindClass(Atom, hInstance, &pi->pclsPrivateList, Link);
/* Step 2: 公共链表按 (原子, 忽略模块) 匹配 ------ 全局类 */
Class = IntFindClass(Atom, NULL, &pi->pclsPublicList, Link);
/* Step 3: 私有链表中找 user32 注册的类(hModClient) */
Class = IntFindClass(Atom, hModClient, &pi->pclsPrivateList, Link);
/* Step 4: 公共链表中找 user32 注册的类 */
Class = IntFindClass(Atom, hModClient, &pi->pclsPublicList, Link);
四步语义:
- 调用者模块自己注册的私有类(最常见);
- 任意模块注册的
CS_GLOBALCLASS公共类; - 兜底:user32 注册但挂在私有链表的类(如
CSF_SERVERSIDEPROC类------UserRegisterSystemClasses
把它们挂公共链表,但 user32 的某些类可能挂在私有链表); - 兜底:user32 注册的公共类。
找到后写回 *BaseClass 并返回原子;找不到返回 (RTL_ATOM)0。
5.4 IntGetAndReferenceClass ------ 窗口创建的类入口(class.c L1449-L1501)
c
PCLS IntGetAndReferenceClass(PUNICODE_STRING ClassName, HINSTANCE hInstance,
BOOL bDesktopThread)
{
if (bDesktopThread) pti = gptiDesktopThread;
else pti = PsGetCurrentThreadWin32Thread();
if (!(pti->ppi->W32PF_flags & W32PF_CLASSESREGISTERED))
UserRegisterSystemClasses(); // 惰性注册系统类
ClassAtom = IntGetClassAtom(ClassName, hInstance, pti->ppi, &Class, &ClassLink);
if (ClassAtom == 0) { ERR(...); return NULL; } // 类不存在
Class = IntReferenceClass(Class, ClassLink, pti->rpdesk); // 引用 + 桌面克隆
if (Class == NULL) { ERR("Failed to reference window class!"); return NULL; }
return Class;
}
这是 IntCreateWindow 在 window.c L2188 调用的入口。流程:确保系统类已注册 → 四步查类 →
IntReferenceClass 增加引用计数并按窗口所在桌面取(或克隆出)类对象。类不存在时返回 NULL,
IntCreateWindow 置 ERROR_CANNOT_FIND_WND_CLASS(window.c L2191),user32 侧据此尝试加载
comctl32 版本化类(GetClassInfoEx 的 VersionRegisterClass 分支)。
5.5 IntReferenceClass / IntGetClassForDesktop ------ 引用与桌面克隆(class.c L629-L778)
c
PCLS IntReferenceClass(IN OUT PCLS BaseClass, IN OUT PCLS *ClassLink, IN PDESKTOP Desktop)
{
if (Desktop != NULL)
Class = IntGetClassForDesktop(BaseClass, ClassLink, Desktop); // 取/克隆桌面副本
else
Class = BaseClass; // 无桌面:直接用基类
if (Class != NULL) Class->cWndReferenceCount++;
return Class;
}
IntGetClassForDesktop(L629-L751)分三种情况:
BaseClass->rpdeskParent == Desktop:类就在本桌面堆上,直接返回基类(最常见路径)。BaseClass->rpdeskParent == NULL:基类在共享堆(线程尚未 attach 桌面时注册的类)。
此时没有克隆可找,Class = NULL,进入克隆逻辑。- 其他桌面 :遍历
BaseClass->pclsClone找rpdeskParent == Desktop的克隆。
找不到时克隆一个新类到目标桌面堆:
c
ClassSize = sizeof(*BaseClass) + BaseClass->cbclsExtra;
Class = DesktopHeapAlloc(Desktop, ClassSize);
RtlCopyMemory(Class, BaseClass, ClassSize); // 整块拷贝(含类附加字节)
/* 对 spcur/spicn/spicnSm 各 UserReferenceObject 一次 */
Class->rpdeskParent = Desktop;
Class->cWndReferenceCount = 0;
if (BaseClass->rpdeskParent == NULL) {
/* 共享堆基类升级为"首个桌面克隆即新基类"(InterlockedExchangePointer 换链表头),
随后 IntDestroyClass 销毁共享堆旧副本 */
Class->pclsBase = Class;
(void)InterlockedExchangePointer((PVOID*)ClassLink, Class);
BaseClass->pclsBase = NULL; BaseClass->pclsClone = NULL;
IntDestroyClass(BaseClass);
} else {
/* 链接进基类的克隆链:Class->pclsBase = BaseClass;
Class->pclsNext = BaseClass->pclsClone; 头插 */
Class->pclsClone = NULL;
(void)InterlockedExchangePointer((PVOID*)&BaseClass->pclsClone, Class);
}
克隆时还会修正 hModule(Bug 4778):默认控件类(fnid 在 FNID_BUTTON~FNID_GHOST)
若 hModule != hModClient,重置为 hModClient。
5.6 IntMakeCloneBaseClass(class.c L780-L816)
当某个克隆成为"唯一存活的类"时,把它提升为新基类:从克隆链摘出、pclsBase = Class、
更新所有其余克隆的 pclsBase、原子交换替换链表头。该函数由 IntDereferenceClass 在
"基类引用归零但还有克隆"时调用(见 7.5)。
5.7 类查找流程走查:原子 → 类链表
用一个具体例子串起整条查找链。假设某应用执行 CreateWindowEx(0, L"EditorClass", ...),
且 "EditorClass" 是它自己注册的私有类:
① user32 CreateWindowExW
└─► NtUserCreateWindowEx(ClassName=L"EditorClass", hInstance=hApp)
② co_UserCreateWindowEx (window.c)
└─► IntGetAndReferenceClass(&ClassName, hApp, FALSE) class.c L1449
├─ pti = PsGetCurrentThreadWin32Thread()
├─ W32PF_CLASSESREGISTERED 未置位?→ UserRegisterSystemClasses() [首次]
├─ IntGetClassAtom(&ClassName, hApp, ppi, &Class, &ClassLink) L1376
│ ├─ IntGetAtomFromStringOrAtom(L"EditorClass", &Atom)
│ │ └─ RtlLookupAtomInAtomTable → Atom = 0xC1A5(举例)
│ ├─ ① IntFindClass(0xC1A5, hApp, &ppi->pclsPrivateList) ── 命中!
│ │ 匹配:atomClassName==0xC1A5 && hModule==hApp &&
│ │ !CSF_WOWDEFERDESTROY;Link 指向链表前驱
│ └─ 返回 Atom=0xC1A5, *BaseClass=类对象
└─ IntReferenceClass(Class, ClassLink, pti->rpdesk) L753
└─ IntGetClassForDesktop(Base, &Link, pti->rpdesk)
├─ rpdeskParent == 当前桌面?→ 直接用(最常见)
└─ 否则克隆到当前桌面堆
└─ 返回 pcls(引用 +1)
③ IntCreateWindow(..., Class, ...) window.c L1805
└─ UserCreateObject(..., sizeof(WND) + Class->cbwndExtra)
└─ pWnd->pcls = Class; pWnd->lpfnWndProc = Class->lpfnWndProc; ...
私有类与公共类的可达性差异:
- 私有类(无
CS_GLOBALCLASS):只挂pclsPrivateList,IntGetClassAtom第 1 步要求
hInstance == Class->hModule精确匹配------其他模块按"自己的 hInstance"查不到; - 公共类(
CS_GLOBALCLASS):挂pclsPublicList,第 2 步以hInstance=NULL匹配------
任意模块均可按类名查得,这正是CS_GLOBALCLASS的语义; - user32 注册的系统类:既可能挂公共链表(
UserRegisterSystemClasses),也可能挂私有链表
(个别控件类),所以第 3、4 步用hModClient兜底,保证CreateWindowEx不带 hInstance
(user32 默认)时系统控件类依然可查。
查找失败的分支 :IntGetAndReferenceClass 返回 NULL 后,IntCreateWindow 置
ERROR_CANNOT_FIND_WND_CLASS;user32 的 CreateWindowExW 内部若拿到该错误码,会尝试
LoadLibrary(comctl32) 注册版本化控件类后重试(VersionRegisterClass 路径)------这就是
"未调用 InitCommonControlsEx 也能创建 comctl32 控件"的兜底来源之一。
6. 类创建与注册流程
6.1 IntRegisterClassAtom(class.c L338-L398)
把类名加入全局原子表 gAtomTable 并返回原子。字符串长度 ≤64 用栈缓冲 szBuf[65],
否则池分配;Length==0 时 IS_ATOM(Buffer) 直接作为原子使用(不再加表)。失败时
SetLastNtError,成功返回 TRUE。
6.2 IntCreateClass ------ 核心创建函数(class.c L1045-L1263)
c
PCLS IntCreateClass(IN CONST WNDCLASSEXW* lpwcx,
IN PUNICODE_STRING ClassName, // 类名(非版本化)
IN PUNICODE_STRING ClassVersion, // 版本化类名
IN PUNICODE_STRING MenuName,
IN DWORD fnID, IN DWORD dwFlags, // CSF_*
IN PDESKTOP Desktop, IN PPROCESSINFO pi)
步骤:
-
注册两个原子 :
IntRegisterClassAtom(ClassName,&Atom)(非版本化)、IntRegisterClassAtom(ClassVersion,&verAtom)(版本化)。任一失败即回滚并返回 NULL。 -
计算尺寸并分配 :
ClassSize = sizeof(*Class) + lpwcx->cbClsExtra;菜单名非空时额外在
UserHeapAlloc中为"Unicode 串 + ANSI 串 + 两个终止符"预留空间(L1082-L1088)。有桌面 →
DesktopHeapAlloc(Desktop, ClassSize);无桌面 →UserHeapAlloc(共享堆,注释FIXME 提示仅桌面窗口等场景允许)。
-
零初始化并填字段(L1108-L1118):
cClass->rpdeskParent = Desktop; Class->pclsBase = Class; Class->atomClassName = verAtom; // 版本化原子 Class->atomNVClassName = Atom; // 非版本化原子 Class->fnid = fnID; Class->CSF_flags = dwFlags; if (LookupFnIdToiCls(Class->fnid, &iCls) && gpsi->atomSysClass[iCls] == 0) gpsi->atomSysClass[iCls] = Class->atomClassName; // 补填系统类原子 -
SEH 保护下拷贝 WNDCLASSEXW 字段 (L1122-L1225):
lpfnWndProc、style、cbclsExtra、cbwndExtra、hModule = lpwcx->hInstance;hIcon/hCursor/hIconSm经UserGetCurIconObject转成
CURICON_OBJECT*引用;hbrBackground原样保存;菜单名拷入预留缓冲并同时生成ANSI 副本(
RtlUnicodeStringToAnsiString),置MenuNameIsString = TRUE。 -
服务端过程匹配 (L1186-L1213):非
CSF_SERVERSIDEPROC时,扫描FNID_FIRST~FNID_SWITCH的服务端表,若
lpfnWndProc恰好等于某个GETPFNCLIENTW/A(i),则升级为GETPFNSERVER(i)并置
CSF_SERVERSIDEPROC(这就是 user32 注册的 Button/Edit 等类在内核中"变服务端"的机制)。 -
收尾 :非 ANSI 置
Unicode = TRUE(L1215-L1216);style & CS_GLOBALCLASS置Global = TRUE(L1218-L1219)。 -
失败回滚 :SEH 异常或转换失败时,释放菜单名缓冲、
DesktopHeapFree(Class)、注销两个原子、
SetLastNtError,返回 NULL。
返回值是已分配的 CLS(尚未挂链表------挂链由调用者完成)。
6.3 UserRegisterClass(class.c L1503-L1588)
NtUserRegisterClassExWOW 的内部实现:
- 查重 (L1524-L1555):用
ClassVersion原子在私有链表找同名类(非全局)→ 已存在报
ERROR_CLASS_ALREADY_EXISTS;若注册的是CS_GLOBALCLASS,再查公共链表的全局类 → 已存在报错。
CSF_SERVERSIDEPROC类跳过查重(服务端类由系统注册,允许重复流程)。 - IntCreateClass (L1557-L1564),传
pti->rpdesk和ppi。 - 挂链表 (L1568-L1580):
Class->Global决定挂pclsPublicList还是pclsPrivateList,
头插 +InterlockedExchangePointer。返回Class->atomNVClassName(非版本化原子)作为
RegisterClass 的返回值。
6.4 NtUserRegisterClassExWOW(class.c L2442-L2603)
系统调用入口,全流程:
- 标志校验 :
Flags & ~CSF_ANSIPROC→ERROR_INVALID_FLAGS(L2469-L2474)。 UserEnterExclusive()持锁;若系统类未注册先UserRegisterSystemClasses()。- SEH 探测 (L2485-L2582):
ProbeForReadUint(&lpwcx->cbSize) != sizeof(WNDCLASSEXW)→ 参数错;ProbeForRead(lpwcx, sizeof(WNDCLASSEXW), sizeof(ULONG))后RtlCopyMemory到本地;ProbeForReadUnicodeString捕获ClassName/ClsVersion/pClassMenuName->pusMenuName;- 校验:长度偶数、
cbClsExtra/cbWndExtra >= 0、hInstance != NULL、溢出检查
(cbClsExtra + 名称 + 菜单 + sizeof(CLS) < cbClsExtra判溢出); - 类名/版本名:字符串需
ProbeForRead,否则必须IS_ATOM; - 菜单名:字符串需 probe,
IS_INTRESOURCE允许(资源 ID 直传)。
- 执行注册 :
Ret = UserRegisterClass(&CapturedClassInfo, &CapturedName, &CapturedVersion, &CapturedMenuName, fnID, Flags)(L2587-L2592)。 UserLeave()释放锁,返回原子(0 = 失败,GetLastError已设置)。
6.5 ProbeAndCaptureUnicodeStringOrAtom(class.c L149-L216)
NtUserUnregisterClass/NtUserGetClassInfo/NtUserGetWOWClass 复用的探测助手:
ProbeForRead(pustrUnsafe, sizeof(UNICODE_STRING), 1);- 长度奇数或 Buffer 为空 →
STATUS_INVALID_PARAMETER; IS_ATOM(Buffer)→ 直接拷贝指针(Length=0);- 否则按长度分配
PagedPool缓冲(TAG_STRING)并拷贝 + 补L'\0'; - SEH 异常时释放已分配缓冲并返回异常码。
6.6 用户态封装(user32/windows/class.c)
RegisterClassExW→RegisterClassExWOWW(lpwcx, NULL, 0, 0, TRUE)(L1425);RegisterClassExA→ 菜单名/类名经MultiByteToWideChar转 Unicode 后同路径(L1378),
并传CSF_ANSIPROC标志;RegisterClassW/A→ 手工逐字段拷贝到WNDCLASSEXW(64 位对齐差异,L1442-L1496);RegisterClassExWOWW(L1270-L1372)内做:参数合法性、hInstance == User32Instance拒绝
(L1308,防止应用冒充 user32 注册系统类)、hInstance == NULL补GetModuleHandleW(NULL)、
菜单名 ANSI/Unicode 双份准备、ClassNameToVersion激活上下文版本化类名解析、
组装CLSMENUNAME后调NtUserRegisterClassExWOW。
版本化类名(ClassNameToVersion,L83-L188) :查激活上下文 WINDOW_CLASS_REDIRECTION 段,
命中则用重定向后的类名(如 comctl32 版本化类 SysListView32 等,is_comctl32_class 内置
20 个名字);ReactOS 无默认激活上下文时用 wine 式 hack 映射到 comctl32 库,VersionRegisterClass
再 LoadLibrary("comctl32") 取 RegisterClassNameW 注册。这套机制解释了一个现象:
IntGetClassAtom 之所以按版本化原子 (atomClassName)匹配,而 GCW_ATOM 返回
非版本化原子 (atomNVClassName)------两者分别对应"重定向后的实际类"与"应用看到的类名"。
7. 类注销与清理
7.1 UserUnregisterClass(class.c L1590-L1644)
(任务描述中的 IntUnregisterClass 在 class.c 中实际命名为 UserUnregisterClass,注意区分。)
c
BOOL UserUnregisterClass(IN PUNICODE_STRING ClassName, IN HINSTANCE hInstance,
OUT PCLSMENUNAME pClassMenuName)
{
ClassAtom = IntGetClassAtom(ClassName, hInstance, pi, &Class, &Link); // 四步查类
if (ClassAtom == 0) { EngSetLastError(ERROR_CLASS_DOES_NOT_EXIST); return FALSE; }
if (Class->cWndReferenceCount != 0 || Class->pclsClone != NULL) {
EngSetLastError(ERROR_CLASS_HAS_WINDOWS); // 还有窗口/克隆,拒绝
return FALSE;
}
ASSERT(Class->pclsBase == Class); // 必须是基类
*Link = Class->pclsNext; // 解链
if (NT_SUCCESS(IntDeregisterClassAtom(Class->atomClassName))) {
Class->atomClassName = 0;
IntDestroyClass(Class); // 释放
return TRUE;
}
return FALSE;
}
约束语义:有窗口(cWndReferenceCount != 0)或存在克隆时拒绝注销 (ERROR_CLASS_HAS_WINDOWS),
这是"类必须先销毁所有窗口才能卸载"的内核实现。
7.2 NtUserUnregisterClass(class.c L2743-L2773)
系统调用:ProbeAndCaptureUnicodeStringOrAtom 捕获类名 → UserEnterExclusive →
UserUnregisterClass(&SafeClassName, hInstance, NULL) → 释放捕获缓冲(非原子时 ExFreePoolWithTag)。
7.3 IntDeregisterClassAtom / IntDestroyClass / IntFreeClassMenuName
c
static NTSTATUS IntDeregisterClassAtom(IN RTL_ATOM Atom) // L420-L425
{ return RtlDeleteAtomFromAtomTable(gAtomTable, Atom); }
static VOID IntFreeClassMenuName(IN OUT PCLS Class) // L220-L230
{
if (Class->lpszClientUnicodeMenuName != NULL && Class->MenuNameIsString)
{ // 只有字符串型菜单名才需要释放(资源 ID 直接存指针)
UserHeapFree(Class->lpszClientUnicodeMenuName);
Class->lpszClientUnicodeMenuName = NULL;
Class->lpszClientAnsiMenuName = NULL;
}
}
static VOID IntDestroyClass(IN OUT PCLS Class) // L232-L301
{
ASSERT(Class->cWndReferenceCount == 0);
ASSERT(Class->pclsClone == NULL);
if (Class->pclsBase == Class) // 只有基类才释放共享资源
{
/* 遍历销毁 spcpdFirst 上的所有 CallProc(DestroyCallProc) */
/* 注销 atomClassName(若非 0)与 atomNVClassName ------ 防止原子复用导致
大小写不敏感比较出错(修复"static test 后跑 class test"问题) */
/* 释放类 DCE(DceFreeClassDCE)与菜单名 */
}
if (Class->spicn) UserDereferenceObject(Class->spicn);
if (Class->spcur) UserDereferenceObject(Class->spcur);
if (Class->spicnSm) {
UserDereferenceObject(Class->spicnSm);
if ((Class->CSF_flags & CSF_CACHEDSMICON) &&
!(UserObjectInDestroy(UserHMGetHandle(Class->spicnSm))))
IntDestroyCurIconObject(Class->spicnSm); // 缓存小图标归类所有,需销毁
}
pDesk = Class->rpdeskParent;
Class->rpdeskParent = NULL;
if (pDesk != NULL) DesktopHeapFree(pDesk, Class); // 桌面堆
else UserHeapFree(Class); // 共享堆
}
7.4 DestroyProcessClasses / IntCheckProcessDesktopClasses(class.c L305-L336 / L1016-L1043)
DestroyProcessClasses(PPROCESSINFO Process):进程退出(最后一个线程销毁)时清理全部类------
先遍历 pclsPrivateList 逐个 IntDestroyClass,再遍历 pclsPublicList。调用点 main.c L821,
前置条件是"进程所有窗口必须先销毁完"(否则 cWndReferenceCount != 0 触发 ASSERT)。
IntCheckProcessDesktopClasses(Desktop, FreeOnFailure):桌面销毁时的配套清理,见下节。
7.5 IntDereferenceClass ------ 窗口销毁时归还类引用(class.c L819-L899)
IntCreateWindow 失败路径、NtUserDestroyWindow 路径(window.c L756)调用。逻辑:
c
ASSERT(Class->cWndReferenceCount >= 1);
BaseClass = Class->pclsBase;
if (--Class->cWndReferenceCount == 0)
{
if (BaseClass == Class) // 是基类
{
if (BaseClass->pclsClone != NULL) // 还有克隆
{
/* 找到本类在 pclsPrivateList/pclsPublicList 中的位置(按 Global 选链表) */
/* IntMakeCloneBaseClass:把第一个克隆提升为新基类(换链表头) */
/* 销毁旧基类(pclsBase/pclsClone 置 NULL 防止误释放共享资源) */
}
// 无克隆:类留在桌面堆,线程 detach 时 IntMoveClassToSharedHeap 迁移
}
else // 是克隆
{
/* 从 BaseClass->pclsClone 链上摘除并 IntDestroyClass */
}
}
即:引用计数归零时,若本类是基类且还有克隆 → 克隆"转正";若本类是克隆 → 直接销毁;
若基类且无克隆 → 暂留桌面堆等待迁移(IntCheckDesktopClasses 处理)。
7.6 IntMoveClassToSharedHeap / IntCheckDesktopClasses(class.c L901-L1014)
IntMoveClassToSharedHeap:把桌面堆上的基类整体拷贝到共享堆(UserHeapAlloc),
引用计数图标/光标,InterlockedExchangePointer 替换链表头,再销毁桌面堆旧副本。
IntCheckDesktopClasses(Desktop, ClassList, FreeOnFailure, Ret):遍历某进程的一类链表,
对 rpdeskParent == Desktop && cWndReferenceCount == 0 的基类执行迁移;迁移失败时
按 FreeOnFailure 决定"直接销毁"还是"置 Ret=FALSE 报 ERROR_NOT_ENOUGH_MEMORY"。
两个链表(私有/公共)都查后由 IntCheckProcessDesktopClasses 汇总。其必要性注释写得很清楚:
桌面堆即将销毁时,若类不迁回共享堆,类对象将不可访问。
7.7 类生命周期状态机
把以上所有路径汇总成一个状态机(粗体为状态,箭头为迁移条件):
UserRegisterClass / UserRegisterSystemClasses
[不存在] ────────────────────────────────────────────────► [已注册·未引用]
│
IntGetAndReferenceClass
│ (IntReferenceClass)
▼
[已注册·被引用]
cWndReferenceCount = N
│
┌────────────────────────────┼──────────────────────────┐
│ │ │
IntDereferenceClass UserUnregisterClass 桌面不匹配(克隆)
(N-1 > 0,仍在用) (N==0 且无克隆) IntGetClassForDesktop
│ │ │
▼ ▼ ▼
[已注册·被引用] [已注销] [已注册·基类+克隆]
│ 解链+销毁类 │
│ │ │
└──── N==0 ────────────────┘ IntDereferenceClass(克隆)
▼ │ (克隆引用归零)
[已注册·未引用] ▼
│ [销毁克隆]
IntCheckProcessDesktopClasses / │
IntMoveClassToSharedHeap │
│ 桌面销毁 │
▼ │
[迁移共享堆] ◄──────────────────────────────────────┘
│
DestroyProcessClasses(进程退出)
▼
[不存在]
关键不变量:
- 销毁前置条件 :
IntDestroyClass断言cWndReferenceCount == 0 && pclsClone == NULL; - 注销前置条件 :
UserUnregisterClass拒绝cWndReferenceCount != 0 || pclsClone != NULL; - 共享资源只在基类 :
IntDestroyClass的"释放 CallProc 链/注销原子/释放 DCE/菜单名"
分支包在if (Class->pclsBase == Class)里,克隆销毁只做引用递减与内存释放; - 克隆转正 :基类引用归零但仍有克隆时
IntMakeCloneBaseClass让克隆顶上,
保证"类在进程内永不消失,只是换了个堆"。
8. 类信息查询与修改
8.1 NtUserGetClassInfo / UserGetClassInfo(class.c L2775-L2877 / L2279-L2330)
NtUserGetClassInfo(GetClassInfoEx 的底层):
ProbeForWrite(lpWndClassEx, sizeof(WNDCLASSEXW), sizeof(ULONG))并拷贝到本地;ProbeAndCaptureUnicodeStringOrAtom捕获类名;hInstance == NULL时视为hModClient;- 持锁 + 惰性注册系统类 +
IntGetClassAtom四步查类;找不到 →ERROR_CLASS_DOES_NOT_EXIST; UserGetClassInfo(Class, &Safewcexw, bAnsi, hInstance)填充结构;- 模仿 Windows 未文档化行为:返回类原子作为 BOOL (L2861-L2863,注释引 Wine:
"We must return the atom of the class here instead of just TRUE")。
UserGetClassInfo 的字段转换(L2287-L2329):
style原样;但若Class->fnid非 0(系统类)则清掉CS_GLOBALCLASS位 (wine class test
check_style的要求,L2290-L2291);lpfnWndProc = IntGetClassWndProc(Class, Ansi)------ 服务端过程映射回客户过程;cbClsExtra/cbWndExtra/hIcon/hCursor/hIconSm/hbrBackground直接搬运;- 菜单名:字符串型(
MenuNameIsString)经UserHeapAddressToUser映射为用户态地址返回; hInstance == hModClient时置 NULL(系统类对应用隐藏 user32 模块基址)。
8.2 NtUserSetClassLong / IntNtUserSetClassLongPtr(class.c L2605-L2728)
NtUserSetClassLong(hWnd, Offset, dwNewLong, Ansi)(L2704)→ IntNtUserSetClassLongPtr(..., sizeof(LONG));
64 位下另有 NtUserSetClassLongPtr 传 sizeof(LONG_PTR)。内部:
UserEnterExclusive();Window = UserGetWindowObject(hWnd);跨进程校验 :Window->head.pti->ppi != pi→
ERROR_ACCESS_DENIED(只能改本进程窗口所属的类);- 对
GCW_ATOM/GCLP_MENUNAME特殊处理:dwNewLong按"原子或UNICODE_STRING*"捕获后
把地址赋给dwNewLong(L2634-L2673); UserSetClassLongPtr(Window->pcls, Offset, dwNewLong, Ansi, Size);GCLP_HICON/GCLP_HICONSM变更且返回值与旧值不同时UserPaintCaption(Window, DC_ICON)
刷新标题栏图标(L2681-L2689)。
8.3 UserSetClassLongPtr(class.c L1895-L2277)
SetClassLong 的核心。先 Class = Class->pclsBase(只改基类,再同步克隆)。分两大分支:
Index ≥ 0 ------ 类附加字节:
c
if (((ULONG)Index + Size) < (ULONG)Index || // 溢出
((ULONG)Index + Size) > (ULONG)Class->cbclsExtra) // 越界
{ EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }
Ret = IntGetSetClassLongPtr(Class, Index, NewLong, Size); // 读旧值、写新值
/* 同步所有克隆 */
IntGetSetClassLongPtr(L1870-L1893)用非对齐读写:Address = (PUCHAR)(&Class[1]) + Index,
ReadUnalignedU32/WriteUnalignedU32(32 位)或 ReadUnalignedUlongPtr(64 位)------类附加字节
就存放在 CLS 结构体之后。
Index < 0 ------ 内置索引,switch 分派:
| 索引 | 行为 |
|---|---|
GCL_CBWNDEXTRA |
读旧值、改 cbwndExtra、同步克隆(注意:新窗口才生效) |
GCL_CBCLSEXTRA |
只报 ERROR_INVALID_PARAMETER(附加字节尺寸不可改) |
GCLP_HBRBACKGROUND |
换 hbrBackground |
GCLP_HCURSOR |
经 UserGetCurIconObject 换 spcur,旧光标 UserDereferenceObject;nop 提前返回 |
GCLP_HICON |
换 spicn;若旧图标带 CSF_CACHEDSMICON 则销毁缓存小图标;新图标没有小图标时用 co_IntCopyImage 从大图标生成小图标(LR_COPYFROMRESOURCE 优先)并置 CSF_CACHEDSMICON |
GCLP_HICONSM |
换 spicnSm;NewLong==0 时从大图标重新生成小图标 |
GCLP_HMODULE |
换 hModule |
GCLP_MENUNAME |
IntSetClassMenuName(见 8.4) |
GCL_STYLE |
换 style;注释 FIXME:改 CS_GLOBALCLASS 不搬链表,靠 Class->Global 原始值定位 |
GCLP_WNDPROC |
IntSetClassWndProc(见 9.2) |
GCW_ATOM |
IntSetClassAtom(换类名原子) |
| 其他 | ERROR_INVALID_INDEX |
所有分支末尾都"同步克隆":遍历 Class->pclsClone,逐个应用同一修改。
8.4 IntSetClassMenuName / IntSetClassAtom
IntSetClassMenuName(L1766-L1868):菜单名为字符串时分配"Unicode+ANSI"双份缓冲、
拷贝并置 MenuNameIsString;为 IS_INTRESOURCE 时直接存资源 ID 指针并清 MenuNameIsString;
改完同步所有克隆。
IntSetClassAtom(L449-L493):IntRegisterClassAtom 新名 → IntDeregisterClassAtom(atomNVClassName)
换名(注释 XXX: are we missing refcount here ? 提示直接取自原子号时未计引用)→ 同步克隆。
8.5 NtUserGetClassName / UserGetClassName(class.c L2880-L2936 / L1646-L1764)
NtUserGetClassName(hWnd, Real, ClassName)(GetClassNameW 传 Real=FALSE,
RealGetWindowClassW 传 Real=TRUE):
Real && Window->fnid && !(Window->fnid & FNID_DESTROY)时:LookupFnIdToiCls(fnid,&iCls)
得到系统类索引,Atom = gpsi->atomSysClass[iCls]------ 即按 fnid 反查真实系统类名
(L2895-L2901)。例如一个Button控件即使被子类化,RealGetWindowClass仍返回 "Button";ProbeForWriteUnicodeString后调UserGetClassName(Window->pcls, &CapturedClassName, Atom, FALSE),
成功后回写ClassName->Length。
UserGetClassName(L1646-L1764)实际执行"原子 → 类名":
- Unicode :
RtlQueryAtomInAtomTable(gAtomTable, Atom ? Atom : Class->atomNVClassName, ..., ClassName->Buffer, &BufLen),返回BufLen/sizeof(WCHAR); - ANSI :先用栈缓冲
szStaticTemp[32]查原子名,STATUS_BUFFER_TOO_SMALL则按调用者
缓冲上限分配临时池缓冲,再RtlUnicodeStringToAnsiString转码; - 全程 SEH,异常时
SetLastNtError。
user32 侧 GetClassNameW(class.c L1023)用 RtlInitEmptyUnicodeString 包住调用者缓冲后
直调 NtUserGetClassName(hWnd, FALSE, &ClassName)。
8.6 其余系统调用
- NtUserGetWOWClass(L2938-L2982) :返回
PCLS指针(16 位 WOW 兼容遗留),
同样走IntGetClassAtom;找不到置ERROR_CLASS_DOES_NOT_EXIST。注释提醒调用方
DesktopPtrToUser转换。 - NtUserSetClassWord(L2730-L2741) :32 位下已废弃,直接返回 0(注释
Obsoleted in 32-bit windows);user32 的SetClassWord转而调SetClassLongW。
8.7 user32 侧 GetClassLongA/W ------ 共享内存直读
注意一个关键差异:内核没有 NtUserGetClassLong 系统调用 。GetClassLong/GetClassLongPtr
在 user32 内部直接读取共享内存中的类对象:Class = DesktopPtrToUser(Wnd->pcls)(windows/class.c
L819、L875)------桌面堆被映射进用户态,WND.pcls 是内核地址,DesktopPtrToUser 把它换算成
用户态可读的 CLS*,随后 IntGetClassLongA/W(L652-L799)就地取值:
-
nIndex >= 0:读类附加字节*(PULONG_PTR)((PULONG_PTR)(Class + 1) + nIndex),越界报
ERROR_INVALID_PARAMETER; -
负索引 switch:
索引 ANSI/W 取值 GCL_CBWNDEXTRA/GCL_CBCLSEXTRAClass->cbwndExtra/Class->cbclsExtraGCLP_HBRBACKGROUNDClass->hbrBackground;< 0x4000时换算GetSysColorBrush(Ret-1)(系统色画刷是 1 基索引)GCLP_HMODULEClass->hModuleGCLP_MENUNAMEClass->lpszClientAnsiMenuName/lpszClientUnicodeMenuNameGCL_STYLEClass->styleGCW_ATOMClass->atomNVClassName(非版本化原子)GCLP_HCURSOR/GCLP_HICON/GCLP_HICONSM((PPROCMARKHEAD)SharedPtrToUser(spcur/spicn/spicnSm))->h(对象→句柄)GCLP_WNDPROCIntGetClsWndProc(Wnd, Class, Ansi)(服务端→客户/A-W 转换,L536-L583)
64 位下 GetClassLongA/W 对 GCLP_* 索引直接报 ERROR_INVALID_INDEX(L822-L841),
必须用 GetClassLongPtrA/W(走 IntGetClassLongA/W)。
这种"共享内存直读"正是 ReactOS 把 USER 对象(含 CLS)放在桌面堆并映射进用户态的收益:
读路径零系统调用,写路径(SetClassLong)因要跨进程校验与同步克隆才进内核。
8.8 GetClassNameA/W 用户态封装
GetClassNameW(windows/class.c L1023-L1047):RtlInitEmptyUnicodeString(&ClassName, lpClassName, nMaxCount * sizeof(WCHAR)) 包住调用者缓冲 → NtUserGetClassName(hWnd, FALSE, &ClassName) → 返回长度。
GetClassNameA(L997-L1017):先调 W 版取到 tmpbuf[MAX_ATOM_LEN + 1],再
RtlUnicodeToMultiByteN 转 ANSI。RealGetWindowClassW/A(L1232-L1267)则把 Real 置 TRUE,
经内核 fnid→系统类原子反查得到真实类名。
8.9 user32 侧 SetClassLongA/W 封装
SetClassLongA/W(windows/class.c L1501-L1606)在进系统调用前做参数整形:
GCL_MENUNAME:dwNewLong被当作字符串指针。ANSI 版(SetClassLongA)先把
PSTR经RtlCreateUnicodeStringFromAsciiz转成UNICODE_STRING(Allocated=TRUE),
IS_INTRESOURCE时直接当作 Buffer;随后dwNewLong = (LONG_PTR)&Value传地址给
NtUserSetClassLong,内核在IntNtUserSetClassLongPtr的GCLP_MENUNAME分支再
ProbeForReadUnicodeString捕获,返回后 user32 释放临时串。GCW_ATOM:同样处理------非原子字符串转UNICODE_STRING,原子直接作为 Buffer。- 其余索引 :原样透传,
Ansi参数区分CSF_ANSIPROC语义。 - 64 位下
SetClassLongPtrA/W(L1608-L1638)走NtUserSetClassLongPtr(传sizeof(LONG_PTR))。
8.10 GCL_* / GCW_ATOM 索引速查表
| 索引 | 值 | 读取端(user32 直读) | 写入端(内核 UserSetClassLongPtr) |
|---|---|---|---|
GCL_MENUNAME / GCLP_MENUNAME |
-8 | lpszClientAnsi/UnicodeMenuName |
IntSetClassMenuName(字符串/资源双态) |
GCLP_HBRBACKGROUND |
-10 | hbrBackground(<0x4000 转系统色) |
直接换值 |
GCLP_HCURSOR |
-12 | spcur->h |
UserGetCurIconObject + 引用计数 |
GCLP_HICON |
-14 | spicn->h |
换图标 + 小图标缓存重建 |
GCLP_HMODULE |
-16 | hModule |
直接换值 |
GCL_CBCLSEXTRA |
-20 | cbclsExtra |
拒绝(ERROR_INVALID_PARAMETER) |
GCL_CBWNDEXTRA |
-18 | cbwndExtra |
换值(对新窗口生效) |
GCLP_WNDPROC |
-24 | IntGetClsWndProc |
IntSetClassWndProc(服务端↔客户) |
GCL_STYLE |
-26 | style |
换值(不搬链表,靠 Global 位) |
GCW_ATOM |
-32 | atomNVClassName |
IntSetClassAtom(换类名) |
GCLP_HICONSM |
-34 | spicnSm->h |
换小图标 / 从大图标重建 |
Index ≥ 0 |
--- | 读 (Class+1)[Index] |
IntGetSetClassLongPtr 读写 |
所有"写入"分支最后都同步遍历 pclsClone,保证其他桌面上的克隆类保持一致。
9. 窗口过程与调用链
9.1 LookupFnIdToiCls / FnidToiCls(class.c L106-L147)
c
static struct { int FnId; int ClsId; } FnidToiCls[] = {
{ FNID_SCROLLBAR, ICLS_SCROLLBAR}, { FNID_ICONTITLE, ICLS_ICONTITLE},
{ FNID_MENU, ICLS_MENU}, { FNID_DESKTOP, ICLS_DESKTOP},
{ FNID_SWITCH, ICLS_SWITCH}, { FNID_MESSAGEWND, ICLS_HWNDMESSAGE},
{ FNID_BUTTON, ICLS_BUTTON}, { FNID_COMBOBOX, ICLS_COMBOBOX},
{ FNID_COMBOLBOX, ICLS_COMBOLBOX}, { FNID_DIALOG, ICLS_DIALOG},
{ FNID_EDIT, ICLS_EDIT}, { FNID_LISTBOX, ICLS_LISTBOX},
{ FNID_MDICLIENT, ICLS_MDICLIENT}, { FNID_STATIC, ICLS_STATIC},
{ FNID_IME, ICLS_IME}, { FNID_GHOST, ICLS_GHOST},
{ FNID_TOOLTIPS, ICLS_TOOLTIPS}
};
BOOL FASTCALL LookupFnIdToiCls(int FnId, int *iCls)
{
int i;
for ( i = 0; i < ARRAYSIZE(FnidToiCls); i++)
{
if (FnidToiCls[i].FnId == FnId)
{
if (iCls) *iCls = FnidToiCls[i].ClsId;
return TRUE;
}
}
if (iCls) *iCls = 0;
return FALSE;
}
线性扫描 17 项,fnid→ICLS 双向用途:NtUserGetClassName(Real=TRUE) 反查系统类名、
IntCreateClass 补填 atomSysClass(L1117)、user32 的 NtUserGetClassInfo 等。
9.2 IntGetClassWndProc / IntSetClassWndProc(class.c L498-L627)
IntGetClassWndProc(Class, Ansi)(与 user32 的 IntGetClsWndProc 对称):
CSF_SERVERSIDEPROC:扫FNID_FIRST~FNID_SWITCH,若GETPFNSERVER(i) == Class->lpfnWndProc,
按 Ansi 返回GETPFNCLIENTA(i)或GETPFNCLIENTW(i)(服务端→客户映射);- 普通类:返回
Class->lpfnWndProc;若fnid在FNID_BUTTON~FNID_GHOST之间(默认控件类),
当请求类型与当前存储过程相反时换回另一版(A↔W); - 若结果与存储过程相同但请求的 A/W 类型与
CSF_ANSIPROC不一致 → 说明需要 A/W 转换 :
调UserGetCPD(Class, (Ansi?UserGetCPDA2U:UserGetCPDU2A)|UserGetCPDClass, Ret)拿一个
CallProc 句柄返回(句柄高 16 位为0xFFFF,IsCallProcHandle可识别)。
IntSetClassWndProc(Class, WndProc, Ansi)(GCLP_WNDPROC 的处理器):
- 记旧值
Ret = IntGetClassWndProc(...); - 若原为服务端 → 降级为客户(清
CSF_SERVERSIDEPROC,按 Ansi 置/清CSF_ANSIPROC); - 传入
WndProc是 CallProc 句柄则取pcpd->pfnClientPrevious还原真实地址; - 反向扫描:新过程若等于某
GETPFNCLIENTW/A(i)→ 升级为GETPFNSERVER(i),
置Unicode=TRUE、CSF_SERVERSIDEPROC(注释Ref: "deftest",兼容 wine 的 deftest);
否则按 Ansi 设置Unicode/CSF_ANSIPROC; - 同步所有克隆(
lpfnWndProc+Unicode)。
9.3 UserAddCallProcToClass(class.c L427-L447)
c
VOID UserAddCallProcToClass(IN OUT PCLS Class, IN PCALLPROCDATA CallProc)
{
PCLS BaseClass;
ASSERT(CallProc->spcpdNext == NULL);
BaseClass = Class->pclsBase;
CallProc->spcpdNext = BaseClass->spcpdFirst; // 头插到基类的 CallProc 链
BaseClass->spcpdFirst = CallProc;
/* 更新所有克隆的链头指针(链本体由基类独享) */
Class = Class->pclsClone;
while (Class != NULL) { Class->spcpdFirst = BaseClass->spcpdFirst; Class = Class->pclsNext; }
}
调用方是 window.c L2047(Edit 控件创建时预建 CallProc)。销毁侧在 IntDestroyClass 中
(基类遍历 spcpdFirst 调 DestroyCallProc)。
9.4 callproc.c ------ CALLPROCDATA 与 A/W 转换
CreateCallProc(Desktop, WndProc, Unicode, pi)(callproc.c L28-L56):
UserCreateObject(gHandleTable, Desktop, pi->ptiList, &Handle, TYPE_CALLPROC, sizeof(CALLPROCDATA))
创建对象,填 pfnClientPrevious = WndProc,wType |= Unicode ? UserGetCPDA2U : UserGetCPDU2A。
UserGetCPD(pvClsWnd, Flags, ProcIn)(callproc.c L105-L156):
先在类链(pCls->spcpdFirst)中 UserSearchForCallProc 找"同过程地址 + 同转换类型"的条目,
找不到则 CreateCallProc 新建并头插。返回 GetCallProcHandle(CallProc)
(= UserHMGetHandle(CallProc) | 0xFFFF0000)。
NtUserGetCPD(hWnd, Flags, ProcIn)(L174-L197):用户态入口,只允许
(Flags & ~(U2A|A2U)) == UserGetCPDClass 的类级查询,否则走窗口级。
A/W 转换如何工作 :当一个 ANSI 类窗口(lpfnWndProc 为 ANSI 过程)被以 Unicode 方式查询
窗口过程(GetWindowLongPtrW(GWLP_WNDPROC))时,user32 的 IntGetWndProc(windows/class.c L588)
发现 Ansi != WNDS_ANSIWINDOWPROC 且过程不是默认控件过程,于是请求内核分配一个
CALLPROCDATA 句柄返回;CallWindowProc 收到高 16 位为 0xFFFF 的"过程句柄"时查
UserGetCallProcInfo 得知原始过程与方向,在派发时做 ANSI↔Unicode 消息转换。
10. 类与窗口的关系
窗口创建(window.c IntCreateWindow)对类的消费,逐条对照:
| window.c 行号 | 代码 | 含义 |
|---|---|---|
| L1868 | sizeof(WND) + Class->cbwndExtra |
按类的 cbwndExtra 多分配窗口附加字节 |
| L1906 | pWnd->pcls = Class |
窗口挂接类(引用在 IntGetAndReferenceClass 已加) |
| L1910 | pWnd->cbwndExtra = pWnd->pcls->cbwndExtra |
窗口记录自身附加字节尺寸 |
| L1928-L1957 | Class->spicn && !Class->spicnSm 时 co_IntCopyImage 生成小图标 |
类缺小图标时按大图标补 |
| L1959-L1960 | CSF_SERVERSIDEPROC → WNDS_SERVERSIDEWINDOWPROC |
服务端过程标志传给窗口 |
| L1971-L1980 | IsCallProcHandle(pcls->lpfnWndProc) → 解 CallProc;否则 pWnd->lpfnWndProc = pcls->lpfnWndProc |
窗口过程从类复制 |
| L1983-L1995 | fnid 在 FNID_BUTTON~FNID_GHOST 时按 bUnicodeWindow 换 A/W 过程 |
默认控件类的 A/W 选择 |
| L2001-L2030 | CSF_ANSIPROC → WNDS_ANSIWINDOWPROC;Ansi 创建者 + 系统控件类 → ANSI,否则 Unicode |
窗口 Unicode 位继承类 |
| L2035-L2049 | Edit 类窗口创建时 CreateCallProc + UserAddCallProcToClass |
特殊:Edit 预建 CallProc |
| L2076-L2093 | Cs->hMenu 为空且 pcls->lpszMenuName 非空 → co_IntCallLoadMenu(pcls->hModule, &MenuName) |
菜单名从类取(含 IS_INTRESOURCE 分支) |
| L2128-L2132 | CS_CLASSDC 且 !pcls->pdce → DceAllocDCE(pWnd, DCE_CLASS_DC) 存入类 |
类共享 DC |
| L2188 | Class = IntGetAndReferenceClass(ClassName, Cs->hInstance, FALSE) |
类解析入口,失败置 ERROR_CANNOT_FIND_WND_CLASS |
| L2619 | IntDereferenceClass(Class, pti->pDeskInfo, pti->ppi) |
窗口销毁路径归还类引用 |
类附加字节 vs 窗口附加字节:
- 类附加字节(
cbClsExtra):在CLS之后,SetClassLong/GetClassLong按正索引访问,
同一类所有窗口共享一份; - 窗口附加字节(
cbWndExtra):在WND之后,SetWindowLong/GetWindowLong(GWL_USERDATA 等
正索引)访问,每个窗口独立一份,RegisterClass时由类定义尺寸。
窗口销毁对类的反向影响 :IntDestroyWindow(window.c L748-L760)先
IntDereferenceClass(Window->pcls, ...) 再 Window->pcls = NULL;只有当最后一个窗口销毁、
引用归零后类才可被 UnregisterClass 或 DestroyProcessClasses 真正释放。因此
UserUnregisterClass 检查 cWndReferenceCount 就是验证"该类没有存活窗口"。
11. 调用链总览与源码索引
11.1 注册与创建调用链(mermaid)
#mermaid-svg-LE8uD1KbD3ypD5oe{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-LE8uD1KbD3ypD5oe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LE8uD1KbD3ypD5oe .error-icon{fill:#552222;}#mermaid-svg-LE8uD1KbD3ypD5oe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LE8uD1KbD3ypD5oe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LE8uD1KbD3ypD5oe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LE8uD1KbD3ypD5oe .marker.cross{stroke:#333333;}#mermaid-svg-LE8uD1KbD3ypD5oe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LE8uD1KbD3ypD5oe p{margin:0;}#mermaid-svg-LE8uD1KbD3ypD5oe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster-label text{fill:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster-label span{color:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster-label span p{background-color:transparent;}#mermaid-svg-LE8uD1KbD3ypD5oe .label text,#mermaid-svg-LE8uD1KbD3ypD5oe span{fill:#333;color:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe .node rect,#mermaid-svg-LE8uD1KbD3ypD5oe .node circle,#mermaid-svg-LE8uD1KbD3ypD5oe .node ellipse,#mermaid-svg-LE8uD1KbD3ypD5oe .node polygon,#mermaid-svg-LE8uD1KbD3ypD5oe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LE8uD1KbD3ypD5oe .rough-node .label text,#mermaid-svg-LE8uD1KbD3ypD5oe .node .label text,#mermaid-svg-LE8uD1KbD3ypD5oe .image-shape .label,#mermaid-svg-LE8uD1KbD3ypD5oe .icon-shape .label{text-anchor:middle;}#mermaid-svg-LE8uD1KbD3ypD5oe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LE8uD1KbD3ypD5oe .rough-node .label,#mermaid-svg-LE8uD1KbD3ypD5oe .node .label,#mermaid-svg-LE8uD1KbD3ypD5oe .image-shape .label,#mermaid-svg-LE8uD1KbD3ypD5oe .icon-shape .label{text-align:center;}#mermaid-svg-LE8uD1KbD3ypD5oe .node.clickable{cursor:pointer;}#mermaid-svg-LE8uD1KbD3ypD5oe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LE8uD1KbD3ypD5oe .arrowheadPath{fill:#333333;}#mermaid-svg-LE8uD1KbD3ypD5oe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LE8uD1KbD3ypD5oe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LE8uD1KbD3ypD5oe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LE8uD1KbD3ypD5oe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LE8uD1KbD3ypD5oe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LE8uD1KbD3ypD5oe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster text{fill:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe .cluster span{color:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe 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-LE8uD1KbD3ypD5oe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LE8uD1KbD3ypD5oe rect.text{fill:none;stroke-width:0;}#mermaid-svg-LE8uD1KbD3ypD5oe .icon-shape,#mermaid-svg-LE8uD1KbD3ypD5oe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LE8uD1KbD3ypD5oe .icon-shape p,#mermaid-svg-LE8uD1KbD3ypD5oe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LE8uD1KbD3ypD5oe .icon-shape .label rect,#mermaid-svg-LE8uD1KbD3ypD5oe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LE8uD1KbD3ypD5oe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LE8uD1KbD3ypD5oe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LE8uD1KbD3ypD5oe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
应用: RegisterClassExW
user32 RegisterClassExWOWW
ClassNameToVersion 版本化类名
NtUserRegisterClassExWOW
系统类已注册?
UserRegisterSystemClasses
Probe 参数 SEH
UserRegisterClass
IntFindClass 查重
IntCreateClass
注册 atomClassName/atomNVClassName 原子
分配 CLS 桌面堆/共享堆
匹配 GETPFNCLIENT→GETPFNSERVER 升级服务端
挂 pclsPrivateList/pclsPublicList
返回 atomNVClassName
应用: CreateWindowExW
NtUserCreateWindowEx
co_UserCreateWindowEx
IntGetAndReferenceClass
IntGetClassAtom 四步查找
IntReferenceClass → IntGetClassForDesktop 克隆
IntCreateWindow
pWnd->pcls = Class
pWnd->lpfnWndProc 从类复制/A-W 校正
菜单/DC/附加字节从类取
11.2 注销与清理调用链
#mermaid-svg-Ag8GE8NW07enB5tj{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-Ag8GE8NW07enB5tj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ag8GE8NW07enB5tj .error-icon{fill:#552222;}#mermaid-svg-Ag8GE8NW07enB5tj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ag8GE8NW07enB5tj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ag8GE8NW07enB5tj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ag8GE8NW07enB5tj .marker.cross{stroke:#333333;}#mermaid-svg-Ag8GE8NW07enB5tj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ag8GE8NW07enB5tj p{margin:0;}#mermaid-svg-Ag8GE8NW07enB5tj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ag8GE8NW07enB5tj .cluster-label text{fill:#333;}#mermaid-svg-Ag8GE8NW07enB5tj .cluster-label span{color:#333;}#mermaid-svg-Ag8GE8NW07enB5tj .cluster-label span p{background-color:transparent;}#mermaid-svg-Ag8GE8NW07enB5tj .label text,#mermaid-svg-Ag8GE8NW07enB5tj span{fill:#333;color:#333;}#mermaid-svg-Ag8GE8NW07enB5tj .node rect,#mermaid-svg-Ag8GE8NW07enB5tj .node circle,#mermaid-svg-Ag8GE8NW07enB5tj .node ellipse,#mermaid-svg-Ag8GE8NW07enB5tj .node polygon,#mermaid-svg-Ag8GE8NW07enB5tj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ag8GE8NW07enB5tj .rough-node .label text,#mermaid-svg-Ag8GE8NW07enB5tj .node .label text,#mermaid-svg-Ag8GE8NW07enB5tj .image-shape .label,#mermaid-svg-Ag8GE8NW07enB5tj .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ag8GE8NW07enB5tj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ag8GE8NW07enB5tj .rough-node .label,#mermaid-svg-Ag8GE8NW07enB5tj .node .label,#mermaid-svg-Ag8GE8NW07enB5tj .image-shape .label,#mermaid-svg-Ag8GE8NW07enB5tj .icon-shape .label{text-align:center;}#mermaid-svg-Ag8GE8NW07enB5tj .node.clickable{cursor:pointer;}#mermaid-svg-Ag8GE8NW07enB5tj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ag8GE8NW07enB5tj .arrowheadPath{fill:#333333;}#mermaid-svg-Ag8GE8NW07enB5tj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ag8GE8NW07enB5tj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ag8GE8NW07enB5tj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ag8GE8NW07enB5tj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ag8GE8NW07enB5tj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ag8GE8NW07enB5tj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ag8GE8NW07enB5tj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ag8GE8NW07enB5tj .cluster text{fill:#333;}#mermaid-svg-Ag8GE8NW07enB5tj .cluster span{color:#333;}#mermaid-svg-Ag8GE8NW07enB5tj 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-Ag8GE8NW07enB5tj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ag8GE8NW07enB5tj rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ag8GE8NW07enB5tj .icon-shape,#mermaid-svg-Ag8GE8NW07enB5tj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ag8GE8NW07enB5tj .icon-shape p,#mermaid-svg-Ag8GE8NW07enB5tj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ag8GE8NW07enB5tj .icon-shape .label rect,#mermaid-svg-Ag8GE8NW07enB5tj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ag8GE8NW07enB5tj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ag8GE8NW07enB5tj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ag8GE8NW07enB5tj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
基类且有克隆
基类无克隆
克隆
UnregisterClassW
NtUserUnregisterClass
ProbeAndCaptureUnicodeStringOrAtom
UserUnregisterClass
引用数=0 且无克隆?
ERROR_CLASS_HAS_WINDOWS
解链 + IntDeregisterClassAtom
IntDestroyClass
销毁 CallProc 链/注销原子/释放 DCE
UserDereferenceObject 图标光标
DesktopHeapFree / UserHeapFree
窗口销毁 IntDestroyWindow
IntDereferenceClass
是基类?
IntMakeCloneBaseClass 克隆转正
留在桌面堆等迁移
摘链并销毁
进程/桌面销毁
DestroyProcessClasses
IntCheckProcessDesktopClasses
IntCheckDesktopClasses → IntMoveClassToSharedHeap
11.3 函数行号索引(class.c)
| 函数 | 行号 | 说明 |
|---|---|---|
ControlsList[] |
L14-L33 | 17 个标准控件名 |
DefaultServerClasses[] |
L37-L104 | 内建系统类表 |
FnidToiCls[] / LookupFnIdToiCls |
L106-L147 | fnid→ICLS 映射 |
ProbeAndCaptureUnicodeStringOrAtom |
L149-L216 | 捕获类名/原子 |
IntFreeClassMenuName |
L220-L230 | 释放字符串菜单名 |
IntDestroyClass |
L232-L301 | 销毁类对象 |
DestroyProcessClasses |
L305-L336 | 进程退出清理 |
IntRegisterClassAtom / IntDeregisterClassAtom |
L338-L398 / L420-L425 | 原子加/删 |
RegisterControlAtoms |
L400-L418 | 预注册控件原子 |
UserAddCallProcToClass |
L427-L447 | 类 CallProc 链 |
IntSetClassAtom |
L449-L493 | GCW_ATOM 处理器 |
IntGetClassWndProc / IntSetClassWndProc |
L498-L543 / L546-L627 | A/W 过程映射 |
IntGetClassForDesktop |
L629-L751 | 桌面克隆 |
IntReferenceClass |
L753-L778 | 引用 +1 |
IntMakeCloneBaseClass |
L780-L816 | 克隆转正 |
IntDereferenceClass |
L819-L899 | 引用 -1 |
IntMoveClassToSharedHeap |
L901-L945 | 迁移共享堆 |
IntCheckDesktopClasses / IntCheckProcessDesktopClasses |
L947-L1014 / L1016-L1043 | 桌面清理 |
IntCreateClass |
L1045-L1263 | 类创建核心 |
IntFindClass |
L1265-L1292 | 链表匹配 |
IntGetAtomFromStringOrAtom |
L1294-L1374 | 名字→原子 |
IntGetClassAtom |
L1376-L1447 | 四步查找 |
IntGetAndReferenceClass |
L1449-L1501 | 窗口创建找类 |
UserRegisterClass |
L1503-L1588 | 注册主流程 |
UserUnregisterClass |
L1590-L1644 | 注销主流程 |
UserGetClassName |
L1646-L1764 | 原子→类名 |
IntSetClassMenuName |
L1766-L1868 | 菜单名处理器 |
IntGetSetClassLongPtr |
L1870-L1893 | 附加字节读写 |
UserSetClassLongPtr |
L1895-L2277 | SetClassLong 核心 |
UserGetClassInfo |
L2279-L2330 | GetClassInfo 填充 |
UserRegisterSystemClasses |
L2335-L2438 | 系统类注册 |
NtUserRegisterClassExWOW |
L2442-L2603 | 系统调用:注册 |
IntNtUserSetClassLongPtr / NtUserSetClassLong |
L2605-L2702 / L2704-L2713 | 系统调用:SetClassLong |
NtUserSetClassLongPtr(64 位) |
L2715-L2728 | 64 位变体 |
NtUserSetClassWord |
L2730-L2741 | 废弃接口 |
NtUserUnregisterClass |
L2743-L2773 | 系统调用:注销 |
NtUserGetClassInfo |
L2775-L2877 | 系统调用:GetClassInfo |
NtUserGetClassName |
L2880-L2936 | 系统调用:GetClassName |
NtUserGetWOWClass |
L2938-L2982 | 返回类指针(WOW) |
11.4 与其他文件的接口
| 外部函数 | 所在文件 | 与 class.c 的关系 |
|---|---|---|
UserCreateObject / UserReferenceObject / UserDereferenceObject |
object.c | CLS 的分配与图标/光标引用计数 |
UserGetCurIconObject / UserHMGetHandle / IntDestroyCurIconObject |
cursoricon.c | 类图标/光标的对象化 |
UserHeapAlloc / UserHeapFree / DesktopHeapAlloc / DesktopHeapFree |
usrheap.c | 类对象的内存来源 |
RtlAddAtomToAtomTable / RtlLookupAtomInAtomTable / RtlQueryAtomInAtomTable / RtlDeleteAtomFromAtomTable |
内核原子(useratom.c) | 类名的原子化 |
CreateCallProc / UserGetCPD / GetCallProcHandle / UserSearchForCallProc |
callproc.c | A/W 转换句柄 |
IntCreateWindow / IntDestroyWindow |
window.c | 类的消费与引用归还 |
co_IntCopyImage |
user32 调用(回调) | 小图标生成 |
DceAllocDCE / DceFreeClassDCE |
windc.c | CS_CLASSDC 的类 DC |
UserPaintCaption |
nonclient.c | SetClassLong 图标变更后刷新标题栏 |
IntGetSysColorBrush |
系统颜色 | 系统类背景画刷 |
RegisterSystemControls |
user32/controls/regcontrol.c | 用户态控件类注册 |
RegisterClassExWOWW / ClassNameToVersion / VersionRegisterClass |
user32/windows/class.c | 用户态封装与版本化 |
12. 总结
class.c 用 2984 行实现了 Win32 窗口类的完整生命周期:
- 注册 :
NtUserRegisterClassExWOW→UserRegisterClass→IntCreateClass,
类名双原子(版本化/非版本化)、附加字节、图标/光标对象化、菜单名双份副本,
以及"客户过程自动升级为服务端过程"的CSF_SERVERSIDEPROC机制; - 查找 :
IntGetAndReferenceClass→IntGetClassAtom四步遍历(私有→公共→user32 私有→
user32 公共),全程以原子比较代替字符串比较; - 桌面隔离 :基类 +
pclsClone克隆机制、cWndReferenceCount引用计数、
共享堆/桌面堆双堆迁移(IntMoveClassToSharedHeap),保证跨桌面窗口创建的正确性与
内存回收的及时性; - 查询与修改 :
GetClassInfo/SetClassLong/GetClassName一应俱全,
SetClassLong支持类附加字节读写与全套GCL*内置索引,且所有修改同步克隆; - 与窗口的关系 :
WND.pcls指向类,窗口创建时从类复制lpfnWndProc、继承
cbwndExtra/cbClsExtra/菜单/DC,销毁时归还引用------"类 = 模板,窗口 = 实例"在
ReactOS 内核中由引用计数严格保证。
下一册将进入 winpos.c,分析窗口创建完成后的"显示"环节:SetWindowPos/ShowWindow 如何
把窗口从"存在"变成"可见"。
本文基于 ReactOS 源码撰写,源码路径:
win32ss/user/ntuser/class.c、win32ss/user/ntuser/class.h、
win32ss/include/ntuser.h、win32ss/user/user32/windows/class.c、
win32ss/user/user32/controls/regcontrol.c、win32ss/user/ntuser/callproc.c、
win32ss/user/ntuser/window.c、win32ss/user/ntuser/ntuser.c、
win32ss/user/ntuser/main.c。行号以 2026 年 8 月仓库状态为准。