ReactOS 窗口系统分析(2):窗口类 — class.c

ReactOS 窗口系统分析(2):窗口类 --- class.c

本文是《ReactOS 窗口系统架构分析》"主线一:窗口显示"系列的第 2 册。

第 1 册(ReactOS 窗口系统分析_1.md)分析了窗口创建主路径 window.c;本册深入剖析"窗口类"------窗口创建之前的那一半故事。

所有内容均基于 d:\reactos\win32ss\user\ntuser\class.c(2984 行)的真实源码,函数行号以该文件为准。

目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. 系统类注册
  5. 类查找与引用
  6. 类创建与注册流程
  7. 类注销与清理
  8. 类信息查询与修改
  9. 窗口过程与调用链
  10. 类与窗口的关系
  11. 调用链总览与源码索引
  12. 总结

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
    ButtonEditStaticListBoxComboBoxMessage 等。系统类进程不可卸载(UnregisterClass
    拒绝或忽略),且常带 CS_GLOBALCLASSCSF_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_GLOBALCLASSUserGetClassInfo
    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 4778IntGetClassForDesktop 克隆类时若默认控件类的 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 结构之后:IntCreateClasssizeof(*Class) + lpwcx->cbClsExtra
    分配;SetClassLong(Index≥0) 通过 IntGetSetClassLongPtr 直接读写 (&Class[1]) + Index
  • cbWndExtra 附加字节 在窗口侧:IntCreateWindowsizeof(WND) + Class->cbwndExtra 分配
    WND(window.c L1868),每个窗口实例独享。
  • pclsBase/pclsClone 构成"基类 + 克隆"结构。只有基类持有 spcpdFirst(CallProc 链)、
    图标/光标引用等共享资源;克隆只是同一数据的桌面堆副本。
  • 图标/光标以对象引用spicn/spcur/spicnSmCURICON_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 的三种来源:

  1. InitUserAtoms(ntuser.c L34-L48):启动时直接给固定原子号------
    ICLS_MENU=32768ICLS_DESKTOP=32769ICLS_DIALOG=32770(即 #32770)、
    ICLS_SWITCH=32771ICLS_ICONTITLE=32772ICLS_TOOLTIPS=32774
    ICLS_HWNDMESSAGE= 全局原子 "Message"
  2. RegisterControlAtoms(class.c L400-L418):把 ControlsList[] 的名字逐个加入原子表,
    填入 atomSysClass[0..16]
  3. 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 ------ 用户态接口结构

WNDCLASSWNDCLASSEX 是其扩展版)是 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_SYSCLASSfiIdPFNCLIENT 成员
    一一对应,见 regcontrol.c RegisterClientPFN 的赋值: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 返回 FALSEhModClient(user32 在内核中的映射基址,ntuser.c L25)
    未初始化说明 user32 尚未完成 NtUserInitializeClientPfnArrays,此时不能注册。
  • Desktop 传 NULL :系统类先分配在共享堆 上(IntCreateClassDesktop==NULL 分支,
    L1096-L1102),等第一个窗口在某个桌面创建时再克隆到该桌面堆(见 5.4)。
  • IFLAGSCSF_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_apitestPopupMenuWndProcW 的期望。
  • 对话框类 不在此表#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 注册、hInstancehModClientUserRegisterSystemClasses

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 == 0Buffer 非空: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);

四步语义:

  1. 调用者模块自己注册的私有类(最常见);
  2. 任意模块注册的 CS_GLOBALCLASS 公共类;
  3. 兜底:user32 注册但挂在私有链表的类(如 CSF_SERVERSIDEPROC 类------UserRegisterSystemClasses
    把它们挂公共链表,但 user32 的某些类可能挂在私有链表);
  4. 兜底: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,

IntCreateWindowERROR_CANNOT_FIND_WND_CLASS(window.c L2191),user32 侧据此尝试加载

comctl32 版本化类(GetClassInfoExVersionRegisterClass 分支)。

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)分三种情况:

  1. BaseClass->rpdeskParent == Desktop:类就在本桌面堆上,直接返回基类(最常见路径)。
  2. BaseClass->rpdeskParent == NULL :基类在共享堆(线程尚未 attach 桌面时注册的类)。
    此时没有克隆可找,Class = NULL,进入克隆逻辑。
  3. 其他桌面 :遍历 BaseClass->pclsClonerpdeskParent == 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):默认控件类(fnidFNID_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):只挂 pclsPrivateListIntGetClassAtom 第 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==0IS_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)

步骤:

  1. 注册两个原子IntRegisterClassAtom(ClassName,&Atom)(非版本化)、

    IntRegisterClassAtom(ClassVersion,&verAtom)(版本化)。任一失败即回滚并返回 NULL。

  2. 计算尺寸并分配ClassSize = sizeof(*Class) + lpwcx->cbClsExtra;菜单名非空时

    额外在 UserHeapAlloc 中为"Unicode 串 + ANSI 串 + 两个终止符"预留空间(L1082-L1088)。

    有桌面 → DesktopHeapAlloc(Desktop, ClassSize);无桌面 → UserHeapAlloc(共享堆,注释

    FIXME 提示仅桌面窗口等场景允许)。

  3. 零初始化并填字段(L1108-L1118):

    c 复制代码
    Class->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;   // 补填系统类原子
  4. SEH 保护下拷贝 WNDCLASSEXW 字段 (L1122-L1225):lpfnWndProcstylecbclsExtra

    cbwndExtrahModule = lpwcx->hInstancehIcon/hCursor/hIconSmUserGetCurIconObject

    转成 CURICON_OBJECT* 引用;hbrBackground 原样保存;菜单名拷入预留缓冲并同时生成

    ANSI 副本(RtlUnicodeStringToAnsiString),置 MenuNameIsString = TRUE

  5. 服务端过程匹配 (L1186-L1213):非 CSF_SERVERSIDEPROC 时,扫描 FNID_FIRST~FNID_SWITCH

    的服务端表,若 lpfnWndProc 恰好等于某个 GETPFNCLIENTW/A(i),则升级为 GETPFNSERVER(i)

    并置 CSF_SERVERSIDEPROC(这就是 user32 注册的 Button/Edit 等类在内核中"变服务端"的机制)。

  6. 收尾 :非 ANSI 置 Unicode = TRUE(L1215-L1216);style & CS_GLOBALCLASS

    Global = TRUE(L1218-L1219)。

  7. 失败回滚 :SEH 异常或转换失败时,释放菜单名缓冲、DesktopHeapFree(Class)

    注销两个原子、SetLastNtError,返回 NULL。

返回值是已分配的 CLS(尚未挂链表------挂链由调用者完成)。

6.3 UserRegisterClass(class.c L1503-L1588)

NtUserRegisterClassExWOW 的内部实现:

  1. 查重 (L1524-L1555):用 ClassVersion 原子在私有链表找同名类(非全局)→ 已存在报
    ERROR_CLASS_ALREADY_EXISTS;若注册的是 CS_GLOBALCLASS,再查公共链表的全局类 → 已存在报错。
    CSF_SERVERSIDEPROC 类跳过查重(服务端类由系统注册,允许重复流程)。
  2. IntCreateClass (L1557-L1564),传 pti->rpdeskppi
  3. 挂链表 (L1568-L1580):Class->Global 决定挂 pclsPublicList 还是 pclsPrivateList
    头插 + InterlockedExchangePointer。返回 Class->atomNVClassName(非版本化原子)作为
    RegisterClass 的返回值。

6.4 NtUserRegisterClassExWOW(class.c L2442-L2603)

系统调用入口,全流程:

  1. 标志校验Flags & ~CSF_ANSIPROCERROR_INVALID_FLAGS(L2469-L2474)。
  2. UserEnterExclusive() 持锁;若系统类未注册先 UserRegisterSystemClasses()
  3. SEH 探测 (L2485-L2582):
    • ProbeForReadUint(&lpwcx->cbSize) != sizeof(WNDCLASSEXW) → 参数错;
    • ProbeForRead(lpwcx, sizeof(WNDCLASSEXW), sizeof(ULONG))RtlCopyMemory 到本地;
    • ProbeForReadUnicodeString 捕获 ClassName/ClsVersion/pClassMenuName->pusMenuName
    • 校验:长度偶数、cbClsExtra/cbWndExtra >= 0hInstance != NULL、溢出检查
      cbClsExtra + 名称 + 菜单 + sizeof(CLS) < cbClsExtra 判溢出);
    • 类名/版本名:字符串需 ProbeForRead,否则必须 IS_ATOM
    • 菜单名:字符串需 probe,IS_INTRESOURCE 允许(资源 ID 直传)。
  4. 执行注册Ret = UserRegisterClass(&CapturedClassInfo, &CapturedName, &CapturedVersion, &CapturedMenuName, fnID, Flags)(L2587-L2592)。
  5. 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)

  • RegisterClassExWRegisterClassExWOWW(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 == NULLGetModuleHandleW(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(进程退出)
                                   ▼
                          [不存在]

关键不变量:

  1. 销毁前置条件IntDestroyClass 断言 cWndReferenceCount == 0 && pclsClone == NULL
  2. 注销前置条件UserUnregisterClass 拒绝 cWndReferenceCount != 0 || pclsClone != NULL
  3. 共享资源只在基类IntDestroyClass 的"释放 CallProc 链/注销原子/释放 DCE/菜单名"
    分支包在 if (Class->pclsBase == Class) 里,克隆销毁只做引用递减与内存释放;
  4. 克隆转正 :基类引用归零但仍有克隆时 IntMakeCloneBaseClass 让克隆顶上,
    保证"类在进程内永不消失,只是换了个堆"。

8. 类信息查询与修改

8.1 NtUserGetClassInfo / UserGetClassInfo(class.c L2775-L2877 / L2279-L2330)

NtUserGetClassInfoGetClassInfoEx 的底层):

  1. ProbeForWrite(lpWndClassEx, sizeof(WNDCLASSEXW), sizeof(ULONG)) 并拷贝到本地;
  2. ProbeAndCaptureUnicodeStringOrAtom 捕获类名;hInstance == NULL 时视为 hModClient
  3. 持锁 + 惰性注册系统类 + IntGetClassAtom 四步查类;找不到 → ERROR_CLASS_DOES_NOT_EXIST
  4. UserGetClassInfo(Class, &Safewcexw, bAnsi, hInstance) 填充结构;
  5. 模仿 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 位下另有 NtUserSetClassLongPtrsizeof(LONG_PTR)。内部:

  1. UserEnterExclusive()
  2. Window = UserGetWindowObject(hWnd)跨进程校验Window->head.pti->ppi != pi
    ERROR_ACCESS_DENIED(只能改本进程窗口所属的类);
  3. GCW_ATOM/GCLP_MENUNAME 特殊处理:dwNewLong 按"原子或 UNICODE_STRING*"捕获后
    把地址赋给 dwNewLong(L2634-L2673);
  4. UserSetClassLongPtr(Window->pcls, Offset, dwNewLong, Ansi, Size)
  5. 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 UserGetCurIconObjectspcur,旧光标 UserDereferenceObject;nop 提前返回
GCLP_HICON spicn;若旧图标带 CSF_CACHEDSMICON 则销毁缓存小图标;新图标没有小图标时用 co_IntCopyImage 从大图标生成小图标(LR_COPYFROMRESOURCE 优先)并置 CSF_CACHEDSMICON
GCLP_HICONSM spicnSmNewLong==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)GetClassNameWReal=FALSE

RealGetWindowClassWReal=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)实际执行"原子 → 类名":

  • UnicodeRtlQueryAtomInAtomTable(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_CBCLSEXTRA Class->cbwndExtra / Class->cbclsExtra
    GCLP_HBRBACKGROUND Class->hbrBackground< 0x4000 时换算 GetSysColorBrush(Ret-1)(系统色画刷是 1 基索引)
    GCLP_HMODULE Class->hModule
    GCLP_MENUNAME Class->lpszClientAnsiMenuName / lpszClientUnicodeMenuName
    GCL_STYLE Class->style
    GCW_ATOM Class->atomNVClassName(非版本化原子)
    GCLP_HCURSOR / GCLP_HICON / GCLP_HICONSM ((PPROCMARKHEAD)SharedPtrToUser(spcur/spicn/spicnSm))->h(对象→句柄)
    GCLP_WNDPROC IntGetClsWndProc(Wnd, Class, Ansi)(服务端→客户/A-W 转换,L536-L583)

64 位下 GetClassLongA/WGCLP_* 索引直接报 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_MENUNAMEdwNewLong 被当作字符串指针。ANSI 版(SetClassLongA)先把
    PSTRRtlCreateUnicodeStringFromAsciiz 转成 UNICODE_STRINGAllocated=TRUE),
    IS_INTRESOURCE 时直接当作 Buffer;随后 dwNewLong = (LONG_PTR)&Value 传地址给
    NtUserSetClassLong,内核在 IntNtUserSetClassLongPtrGCLP_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 对称):

  1. CSF_SERVERSIDEPROC:扫 FNID_FIRST~FNID_SWITCH,若 GETPFNSERVER(i) == Class->lpfnWndProc
    按 Ansi 返回 GETPFNCLIENTA(i)GETPFNCLIENTW(i)(服务端→客户映射);
  2. 普通类:返回 Class->lpfnWndProc;若 fnidFNID_BUTTON~FNID_GHOST 之间(默认控件类),
    当请求类型与当前存储过程相反时换回另一版(A↔W);
  3. 若结果与存储过程相同但请求的 A/W 类型与 CSF_ANSIPROC 不一致 → 说明需要 A/W 转换
    UserGetCPD(Class, (Ansi?UserGetCPDA2U:UserGetCPDU2A)|UserGetCPDClass, Ret) 拿一个
    CallProc 句柄返回(句柄高 16 位为 0xFFFFIsCallProcHandle 可识别)。

IntSetClassWndProc(Class, WndProc, Ansi)GCLP_WNDPROC 的处理器):

  1. 记旧值 Ret = IntGetClassWndProc(...)
  2. 若原为服务端 → 降级为客户(清 CSF_SERVERSIDEPROC,按 Ansi 置/清 CSF_ANSIPROC);
  3. 传入 WndProc 是 CallProc 句柄则取 pcpd->pfnClientPrevious 还原真实地址;
  4. 反向扫描:新过程若等于某 GETPFNCLIENTW/A(i) → 升级为 GETPFNSERVER(i)
    Unicode=TRUECSF_SERVERSIDEPROC(注释 Ref: "deftest",兼容 wine 的 deftest);
    否则按 Ansi 设置 Unicode/CSF_ANSIPROC
  5. 同步所有克隆(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

(基类遍历 spcpdFirstDestroyCallProc)。

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 = WndProcwType |= 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->spicnSmco_IntCopyImage 生成小图标 类缺小图标时按大图标补
L1959-L1960 CSF_SERVERSIDEPROCWNDS_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_ANSIPROCWNDS_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->pdceDceAllocDCE(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;只有当最后一个窗口销毁、

引用归零后类才可被 UnregisterClassDestroyProcessClasses 真正释放。因此

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 窗口类的完整生命周期:

  1. 注册NtUserRegisterClassExWOWUserRegisterClassIntCreateClass
    类名双原子(版本化/非版本化)、附加字节、图标/光标对象化、菜单名双份副本,
    以及"客户过程自动升级为服务端过程"的 CSF_SERVERSIDEPROC 机制;
  2. 查找IntGetAndReferenceClassIntGetClassAtom 四步遍历(私有→公共→user32 私有→
    user32 公共),全程以原子比较代替字符串比较;
  3. 桌面隔离 :基类 + pclsClone 克隆机制、cWndReferenceCount 引用计数、
    共享堆/桌面堆双堆迁移(IntMoveClassToSharedHeap),保证跨桌面窗口创建的正确性与
    内存回收的及时性;
  4. 查询与修改GetClassInfo/SetClassLong/GetClassName 一应俱全,
    SetClassLong 支持类附加字节读写与全套 GCL* 内置索引,且所有修改同步克隆;
  5. 与窗口的关系WND.pcls 指向类,窗口创建时从类复制 lpfnWndProc、继承
    cbwndExtra/cbClsExtra/菜单/DC,销毁时归还引用------"类 = 模板,窗口 = 实例"在
    ReactOS 内核中由引用计数严格保证。

下一册将进入 winpos.c,分析窗口创建完成后的"显示"环节:SetWindowPos/ShowWindow 如何

把窗口从"存在"变成"可见"。


本文基于 ReactOS 源码撰写,源码路径:

win32ss/user/ntuser/class.cwin32ss/user/ntuser/class.h

win32ss/include/ntuser.hwin32ss/user/user32/windows/class.c

win32ss/user/user32/controls/regcontrol.cwin32ss/user/ntuser/callproc.c

win32ss/user/ntuser/window.cwin32ss/user/ntuser/ntuser.c

win32ss/user/ntuser/main.c

行号以 2026 年 8 月仓库状态为准。

相关推荐
傻啦嘿哟1 小时前
王者荣耀爬虫:爬取全英雄皮肤数据,用Python做可视化分析
开发语言·爬虫·python
AI直播技术杂谈1 小时前
直播画面突然模糊了,排查了三个小时
开发语言·php
菜冻鱼1 小时前
Python-pytorch-数据加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
heimeiyingwang1 小时前
【架构实战】可观测性三支柱实战:Metrics、Logging、Tracing 如何统一落地
开发语言·架构·php
caimouse2 小时前
ReactOS 图形系统分析(53):系统库存对象 — stockobj.c
c语言·开发语言·spring
ZJU_统一阿萨姆2 小时前
【推理优化进阶】通信关键路径:NCCL、RDMA 与计算通信重叠
开发语言·人工智能·语言模型·架构·系统架构
我不是疯子是傻子2 小时前
Qt 程序打包部署全攻略:从 windeployqt 到 Inno Setup 的静默安装与版本迭代实战
开发语言·qt
j7~3 小时前
【C语言】《C语言函数递归完全指南:思想、限制、经典例题(阶乘/斐波那契/青蛙跳台阶/汉诺塔)与迭代优化》
c语言·函数递归·汉诺塔问题·青蛙跳台阶问题·递归与迭代·n的阶乘
城管不管3 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
霸道流氓气质4 小时前
Java开发者AI编程常用MCP、Skills、Rules全景汇总
java·开发语言·ai编程