☰
SystemInformer 开发规范实战指南:从 Ph 前缀到引用计数,一次讲清源码阅读与贡献的约定
2026/10/21 3:47:12 网站建设 项目流程

SystemInformer 开发规范实战指南:从 Ph 前缀到引用计数,一次讲清源码阅读与贡献的约定

【免费下载链接】systeminformerA free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solutions, Inc. @ https://windows-internals.com项目地址: https://gitcode.com/GitHub_Trending/sy/systeminformer

导读

SystemInformer(Process Hacker 的后续项目)是一个由几十位贡献者长期协作维护的 C 代码库,它的可读性不是靠注释堆出来的,而是靠一份写在 HACKING.md 里的、近乎可执行的开发规范。读完这篇文章,你能做三件事:仅凭符号名判断它是公开 API、模块私有还是文件内局部实现;看懂四种函数失败语义和 SEH 的边界;按约定写出能直接通过评审的线程入口、锁使用、字符串操作与引用计数代码。

从符号名读起:谁公开,谁私有

先给规则:一个符号的"前缀"决定了它的可见性等级,这是整个代码库最省读者时间的约定。

命名风格分三档:

对象风格示例
函数、参数、全局变量CamelCasePhCreateThread
局部变量lowerCamelCasemyString
结构体、枚举、联合体CAPS_WITH_UNDERSCORESPH_STRING

前缀体系只有一条推导规则——在公开前缀后追加字母p即得私有前缀:Ph→Php、Pha→Phap、PH_→PHP_。配套三条硬性规则:

  • 无前置的函数与全局变量必须声明为static,且static名称禁止携带公开前缀;
  • 带私有前缀的名称不要求static,可以跨文件可见但属于模块内部;
  • 无前置的结构体必须定义在.c/.cpp文件里。

为什么:仅看首字母就能判断一个符号属于公开 API、模块私有还是文件内局部实现,评审代码时能立刻建立可信边界,不用反复翻头文件确认声明位置。

数据怎么表达:固定宽度类型、布尔与注解

规则一:除 Win32 API 调用场景外,一律使用固定宽度类型。原因很直接:Windows 与 NT 内核的调用约定里,"宽度"是协议的一部分,int或long在不同平台上的位宽不一致。

用途类型宽度
布尔BOOLEAN/LOGICAL1 字节 / 4 字节
无符号UCHAR、USHORT、ULONG、ULONG641/2/4/8 字节
有符号SHORT、LONG、LONG642/4/8 字节
字符 / 字符串CHAR/WCHAR、PSTR/PWSTR1/2 字节

规则二:测试布尔值直接写变量本身,禁止与TRUE比较。

if (booleanVariable) // 而不是 if (booleanVariable == TRUE) { ... }

为什么:TRUE的宏值是 1,但不少 API 返回非 1 的真值,比较写法反而制造歧义。若要把NTSTATUS、指针这类表达式规整成严格的 0/1 布尔值,用!!expr双重取反即可。

规则三与两条注解约定:

  • 所有函数签名必须带 SAL 注解(_In_、_Inout_、_Out_等)。它们不是装饰,MSVC 的 /analyze 等静态分析工具依赖这些注解检查内存与别名问题,声明处同时配合DECLSPEC_ALLOCATOR、DECLSPEC_NOALIAS、DECLSPEC_RESTRICT等限定符。
  • 定义中禁止使用volatile。确有必要时,把指针强制转换为指向volatile的指针再访问,只在真正需要"每次重新读取"的场合生效,避免无差别抑制编译器优化。

字符串对象的表达约定放在后面生命周期一节里讲,因为PH_STRING由引用计数管理,和对象规则一体。

失败怎么报告:四种返回语义与两条契约

规则:失败只能靠返回值报告,全代码库只有四种合法语义:

返回值成功判定备注
BOOLEANTRUE为成功最轻量的语义
NTSTATUSNT_SUCCESS宏与 NT 内核接口对齐
HRESULTHR_SUCCESS宏禁止使用SUCCEEDED宏:第三方代码会用S_FALSE表示错误,SUCCEEDED会把它误判为成功
结果值(如指针)非特殊值失败用特殊值(如NULL)表示

语义之外还有两条契约,评审时常被抓的就是这两条:

  1. 失败了的函数保证不修改任何输出参数(_Out_、_Out_opt_等),除非文档另有说明。写 API 时若中途会填充输出,必须在注释里显式声明这一例外。
  2. 带回调的函数,失败时不保证回调未被调用。所以回调内部必须自带状态防护,不能假设"整体失败 = 自己没跑过"。

SEH 的三种例外:什么时候允许抛异常

默认策略是:SEH 异常只留给"无法恢复"的极端情况(如锁获取函数无法阻塞、引用计数出现负数)。允许抛异常的白名单只有三处,每处都有明确理由:

  • PhAllocate(phlib/include/phbasesup.h):分配失败直接抛异常。理由是分配失败极其罕见,为每次分配都检查返回值不划算。确实需要防御性处理的路径用PhAllocateSafe,它带_Must_inspect_result_注解强制检查返回值。
  • PhProbeAddress(phlib/include/phsup.h):地址超出指定范围时抛异常。好处是一个 SEH 块可以合并完成多次地址范围检查,不用每次检查都写分支。
  • STATUS_NOT_IMPLEMENTED:用于纯程序员错误、本不该被到达的路径,等价写法是assert(FALSE)。

除此之外,任何函数都应当返回错误码而不是抛出异常。

线程入口怎么写:固定签名加统一创建入口

规则:每个线程启动例程必须使用固定签名:

_Function_class_(USER_THREAD_START_ROUTINE) NTSTATUS NTAPI NameOfRoutine( _In_ PVOID Parameter );

创建入口同样不许各自为政,统一走PhCreateThread家族(声明见 phlib/include/phbasesup.h):

HANDLE NTAPI PhCreateThread(SIZE_T StackSize, PUSER_THREAD_START_ROUTINE Start, PVOID Parameter); NTSTATUS NTAPI PhCreateThreadEx(PHANDLE ThreadHandle, PUSER_THREAD_START_ROUTINE Start, PVOID Parameter);

PhCreateThread直接返回句柄;PhCreateThreadEx通过输出参数返回句柄并用NTSTATUS报告结果;另有PhCreateThread2(忽略句柄)与线程池投递函数PhQueueUserWorkItem。为什么:所有线程的生命周期管理(创建、等待、清理)收敛到同一组入口,审计时不用满库找CreateThread的裸调用。

侵入式与非侵入式集合怎么选

集合选型的判断维度是"链接字段放哪里"。侵入式集合把链接字段嵌进用户结构体,省去每个元素的一次额外分配;非侵入式集合由容器统一管理元素存储。

数据结构非侵入式侵入式
数组PH_ARRAY/PH_LIST/PH_POINTER_LIST—
链表 / 栈 / 队列—LIST_ENTRY(双向)/SINGLE_LIST_ENTRY(单向)
二叉树(AVL)RTL_AVL_TABLEPH_AVL_LINKS
二叉树(splay)RTL_GENERIC_TABLE—
哈希表PH_HASHTABLEPH_HASH_ENTRY
环形缓冲区PH_CIRCULAR_BUFFER—

选择建议:元素频繁增删、数量动态变化大时优先侵入式链表;需要随机访问与索引语义时用PH_ARRAY、PH_POINTER_LIST。完整清单见 HACKING.md 的 Collections 一节。

锁该用哪把:Queued Lock 与它的同步家族

规则:所有同步都必须使用 queued lock,理由是尺寸小、性能好。PH_QUEUED_LOCK(phlib/include/queuedlock.h)的全部状态就是一个ULONG_PTR Value。它本质是读写锁,但只用独占获取/释放两个函数就能当互斥量:

PhInitializeQueuedLock(&lock); PhAcquireQueuedLockExclusive(&lock); /* 临界区 */ PhReleaseQueuedLockExclusive(&lock);

可以把等待机制想象成一个"按号排队的等待区":阻塞线程按到达顺序挂在 16 字节对齐的等待块链表(_PH_QUEUED_WAIT_BLOCK)上,PH_QUEUED_WAITER_EXCLUSIVE、PH_QUEUED_WAITER_SPINNING等标志实现公平排队与自旋优化,先来的先唤醒,避免饥饿——"queued"(排队)由此得名。

事件与 rundown 保护

  • PH_EVENT:惰性内核事件。它不会在真正阻塞前创建内核事件对象,平时只测试用户态标志位(如PH_EVENT_SET),所以状态测试非常快。适合"高频设置、低频等待"的场景。
  • PH_RUNDOWN_PROTECT:对象销毁时的"浇灭"同步。它允许已有读者进入临界区,但阻止新读者进入,并等待已有读者全部退出后销毁才能完成——监控工具里"后台刷新线程正在遍历列表,而用户正要销毁窗口"的场景就靠它。

条件变量与 Wake Event

条件变量不需要额外结构:PH_CONDITION就是_PH_QUEUED_LOCK的别名,用PhPulseCondition/PhPulseAllCondition与PhWaitForCondition工作(二者是映射到Phf*的宏,见 queuedlock.h),带超时版本为PhWaitForConditionEx。锁与条件变量共享同一数据结构,没有额外的同步对象要管理。

Wake Event 是同一基座上的另一种玩法,适合在循环里轮询多个条件的低开销自定义锁:

/* 循环中测试一个或多个条件,不满足则 */ PhQueueWakeEvent(&wake, &waitBlock); /* 排队准备阻塞 */ PhWaitForWakeEvent(&wake, &waitBlock, FALSE, NULL); /* 条件变化的一侧调用 */ PhSetWakeEvent(&wake, NULL);

PH_WAKE_EVENT同样是 queued lock 别名;若调用PhQueueWakeEvent之后发现不该阻塞,就把等待块传回PhSetWakeEvent取消本次等待。整套组合不引入内核事件对象,就能实现"条件驱动 + 高效唤醒"。

对象的一生:分配、引用、解引用与动词配对

规则:基础分配统一走PhAllocate/PhFree,复杂对象一律走引用计数系统(phlib/include/ref.h)。PhCreateObject创建时引用数为 1,PhReferenceObject加一,PhDereferenceObject减一,归零即销毁。

auto-dereference 池是半自动引用计数,可以理解为"统一延后释放的托盘":PhAutoDereferenceObject把对象放进当前线程的池(类似 Apple 的NSAutoreleasePool),对象在未来某个未指定时刻自动解引用,但保证当前函数执行期间不解引用。

引用时机的两条铁律:

  1. 只要把对象指针存入全局可见位置,或传递给另一个线程,就必须引用它;大多数其他场景不需要引用。
  2. 传给函数的对象,在调用期间必须至少持有一个引用。典型错误:窗口过程里保存了一个可能已被销毁的对象引用并隐式使用它——窗口过程执行期间消息泵仍在工作(比如弹了对话框),对象随时可能被销毁,所以必须在进入窗口过程后尽快引用。

生命周期动词的配对必须精确,评审时会按表核对:

动词含义配对收尾
Allocate分配内存,不初始化Free
Create分配并初始化Free/Destroy
Initialize用调用者提供的存储初始化Delete(不释放内存,存储归调用者)
Create(引用计数语境)经引用计数系统创建PhDereferenceObject

对照实例:PhCreateFileDialog/PhFreeFileDialog、PhInitializeWorkQueue/PhDeleteWorkQueue、PhCreateString/PhDereferenceObject。

计数单位也要分清,这是 API 签名里的"计量协议":

  • Length 以字节计:如PhCreateString的长度参数必须是字节数;
  • Count 以元素计:如PhSubstring的长度参数是字符数;
  • Index 以元素计:如PhSubstring的起始索引是字符数。

另外,向输出写入 null 结尾字符串时,若有返回计数,必须把 null 终止符本身计入写入字符数。

字符串对象:创建、拼接、格式化与自动收尾

字符串使用PH_STRING类型,引用计数管理。结构定义见 phlib/include/phbasesup.h:头部是Length(字节数)加Buffer(可直接按PH_STRINGREF看待),数据区嵌入WCHAR Data[1],与缓冲连续分配,另保留分配跟踪字段。所有字符串内嵌长度、始终以字节计,并额外以 null 结尾兼容传统 C 接口。

PPH_STRING myString = PhCreateString(L"My string"); wprintf(L"My string is \"%s\", and uses %Iu bytes.\n", myString->Buffer, myString->Length);

PhCreateString是内联函数,内部即PhCreateStringEx(Buffer, PhCountStringZ(Buffer) * sizeof(WCHAR)),自动按字节算长度;空串场景用PhReferenceEmptyString拿共享空串,避免重复分配。

不可变性是硬性约定:字符串对象必须视为不可变。唯一例外——用PhCreateString/PhCreateStringEx创建后、指针被其他函数看到或存入全局变量之前,允许本地修改;指针一旦"见光",即只读。

拼接与格式化:

PPH_STRING newString; newString = PhConcatStrings( 4, L"My first string, ", L"My second string, ", aStringFromSomewhere, L"My fourth string." ); newString = PhConcatStrings2(L"My first string, ", L"My second string."); newString = PhFormatString(L"%d: %s, %#x", 100, L"test", 0xff);

PhFormatString的格式串带_Printf_format_string_注解,编译器与静态分析器会校验参数匹配。面向PH_STRINGREF还有PhConcatStringRef系列变体,PhConcatStrings_V内部带长度缓存,避免反复重分配。

最省样板的写法是Pha前缀函数(如PhaConcatStrings):它们把创建结果直接包进PH_AUTO_T放入 auto-dereference 池,作用域结束自动解引用,不用再手写PhDereferenceObject收尾。

提交前过一遍这份清单

  • 命名:函数/参数/全局变量 CamelCase,局部变量 lowerCamelCase,结构体大写带下划线
  • 公开符号带Ph/PH_,模块私有带Php/PHP_,无前置的函数与全局变量已声明static
  • 类型是固定宽度(BOOLEAN/LOGICAL/UCHAR/USHORT/LONG/ULONG/LONG64/ULONG64),Win32 场景除外
  • 布尔直接测试,未写== TRUE;非布尔表达式用!!规整为 0/1
  • 函数签名带 SAL 注解;定义中未出现volatile
  • 失败语义四选一,HRESULT检查用的是HR_SUCCESS而非SUCCEEDED
  • 失败路径未触碰输出参数;带回调的失败路径,回调内部已做状态防护
  • 线程入口是固定签名,创建走PhCreateThread家族
  • 同步用 queued lock 独占获取当互斥量;事件用PH_EVENT,销毁同步用 rundown protect,未裸建内核事件
  • 跨线程或存入全局位置的引用已引用;传给函数的对象在调用期间至少持有一个引用
  • 生命周期动词配对正确(Initialize/Delete、Create/Free),长度参数区分了字节与元素单位
  • 字符串指针共享后只读;创建收尾交给PhDereferenceObject或Pha*自动池

约定不是风格偏好,而是架构本身:Ph/Php前缀划定 API 边界,queued lock 是唯一的同步基石,引用计数与 auto-dereference 池统一管理对象生命周期。SystemInformer 几十万行代码之所以还能被持续协作维护,靠的就是这份写在 HACKING.md 里的清单——按它提交,代码就有资格进入这个库。

【免费下载链接】systeminformerA free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solutions, Inc. @ https://windows-internals.com项目地址: https://gitcode.com/GitHub_Trending/sy/systeminformer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询