首页
/ Windows Terminal 编码风格深析:Modern C++、WIL 与 C++ Core Guidelines 在 OpenConsole 中的落地

Windows Terminal 编码风格深析:Modern C++、WIL 与 C++ Core Guidelines 在 OpenConsole 中的落地

2026-09-05 15:44:38作者:秋泉律Samson

doc/STYLE.md 是 Windows Terminal(连同原 console host,即 OpenConsole 仓库)对贡献者制定的 C++ 编码风格宪法,全文虽短,却规定了五条硬性哲学:跟随既有代码风格、新代码遵循 Modern C++ 与 C++ Core Guidelines、Win32/NT API 调用一律走 WIL 智能指针与结果处理、禁止以 NTSTATUS 作为结果码、C++/WinRT 代码必须正确处理 weak reference 与线程封送。读懂这五条规则,你就能理解从 conhost.exe 到 Windows Terminal UI 全部 C++ 代码背后的统一设计语言,并写出能通过仓库 AuditMode 静态分析检查的合规代码。

五条风格哲学的完整解读

doc/STYLE.md 以“Philosophy”一节列出全部规则,以下逐条继承并展开。

1. 插入既有代码时:跟随上下文

原文第一条规则:“If it's inserting something into the existing classes/functions, try to follow the existing style as closely as possible.”(在向现有类或函数中插入代码时,尽可能贴近既有风格。)

这条规则针对的是本仓库的现实:代码库同时包含历史悠久、异常无关(exception-free)的 console host 模块(src/host/src/server/)和相对现代化的 Terminal 应用模块(src/cascadia/)。两者的局部习惯并不完全一致,向旧模块补代码时强推新风格反而会制造割裂。因此仓库约定:局部修改服从局部惯例,只有整块重写时才适用下面的“Modern C++”标准。

2. 全新代码与重构:Modern C++ + C++ Core Guidelines

原文第二条规则:“If it's brand new code or refactoring a complete class or area of the code, please follow as Modern C++ of a style as you can and reference the C++ Core Guidelines as much as you possibly can.”(品牌新的代码或对整个类/代码区域的重构,请尽可能采用 Modern C++ 风格,并尽可能参考 C++ Core Guidelines。)

值得注意的是,这条规则不是口号,而是由构建系统强制的。仓库在 src/common.build.pre.props 中为 AuditMode 配置开启了 CppCoreCheck 与代码分析:

<PropertyGroup Condition="'$(Configuration)'=='AuditMode'">
  <CodeAnalysisRuleSet>$(SolutionDir)\src\StaticAnalysis.ruleset</CodeAnalysisRuleSet>
  <EnableCppCoreCheck>true</EnableCppCoreCheck>
  <RunCodeAnalysis>true</RunCodeAnalysis>
  ...
</PropertyGroup>

CppCoreCheck 是微软基于 C++ Core Guidelines 实现的编译器检查集,其完整告警编号清单被生成在 src/inc/CppCoreCheck/warnings.h。该文件(由 defectdefs.xml 生成、禁止手工编辑)定义了从 26400 到 26498 的 66 条警告,按类别组织为 CPPCORECHECK_RAW_POINTER_WARNINGSCPPCORECHECK_BOUNDS_WARNINGSCPPCORECHECK_LIFETIME_WARNINGS 等宏组。对照这些告警即可知道“Modern C++ 风格”在本仓库的具体含义,例如:

告警编号 对应 C++ Core Guidelines 条目 含义
26400(WARNING_NO_RAW_POINTER_ASSIGNMENT) i.11 拥有型指针不要用裸指针承接,使用 owner
26408(WARNING_NO_MALLOC_FREE) r.10 避免 malloc/free,优先 nothrow 版 new 配 delete
26409(WARNING_NO_NEW_DELETE) r.11 避免显式 new/delete,改用 std::make_unique<T>
26433(WARNING_OVERRIDE_EXPLICITLY) c.128 覆写函数必须显式标注 override
26437(WARNING_DONT_SLICE) es.63 禁止对象切片(slicing)
26438(WARNING_NO_GOTO) es.76 避免 goto
26440(WARNING_DECLARE_NOEXCEPT) f.6 可声明 noexcept 的函数应当声明
26450–26454 io.1–io.5 编译期可判定的算术溢出检查
26475(WARNING_NO_FUNCTION_STYLE_CASTS) es.49 禁止 C 风格函数式强转
26481(WARNING_NO_POINTER_ARITHMETIC) bounds.1 禁止裸指针算术,改用 span
26486–26489 lifetime.1 指针/引用的生命周期前置后置条件检查

这些规则解释了仓库代码中为何大量使用 std::make_unique/wil::make_unique_nothrow、显式 overridenoexcept——它们不是个人偏好,而是 AuditMode 下会被编译器持续盯梢的硬性要求。

3. Win32/NT API:一律使用 WIL

原文第三条规则:“When working with any Win32 or NT API, please try to use the Windows Implementation Library smart pointers and result handlers.”(操作任何 Win32 或 NT API 时,请使用 WIL 的智能指针与结果处理器。)

WIL(Windows Implementation Library)是头文件库,用于让 Windows API 调用更可预测、更少出错。仓库内有专门文档 doc/WIL.md 讲解其两大用法,以下分别结合仓库源码验证。

WIL 智能指针:RAII 管理 OS 资源

wil/resource.h 提供文件句柄、套接字句柄、进程句柄等 OS 资源的 RAII 包装,典型形态是 wil::unique_handle,出作用域时自动调用匹配的系统函数(如 CloseHandle())。仓库中这类用法随处可见,例如 conpty 输出线程句柄成员:

即连“创建线程句柄后忘记 CloseHandle”这类泄漏,在 src/cascadia/(Terminal 侧)和 src/host/(conhost 侧)都是同一个写法。

[doc/WIL.md](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/WIL.md?utm_source=gitcode_repo_files) 还特别提到 wil::make_unique_nothrow():它是 std::make_unique 的无异常版本,返回 wistd::unique_ptr(而非 std::unique_ptr),便于接入 console 中既有的 exception-free 代码。wistd:: 命名空间的定位是“在 std 有对应物、但带特殊能力(如无异常)”的类型,其余工具都在 wil:: 命名空间。仓库 src/winconpty/winconpty.cpp 展示了配套的字符串/内存资源形态:

wil::GetSystemDirectoryW<wil::unique_process_heap_string>(systemDirectory);
return wil::str_concat_failfast<wil::unique_process_heap_string>(L"\\\\?\\", systemDirectory, L"\\conhost.exe");

这里 wil::unique_process_heap_string 是进程堆分配的 wchar_t* 的所有者类型,出作用域自动释放。

WIL 结果处理:RETURN_IF_* 与 LOG_IF_*

wil/result.h 提供一组宏,统一接管 Windows API 五花八门的失败约定。doc/WIL.md 给出的范例是 DuplicateHandle():它返回 BOOL,失败时还需 GetLastError() 才能真正拿到错误码——用 RETURN_IF_WIN32_BOOL_FALSE 包住该调用即可自动完成“检查 BOOL、抓取 LastError、转换为 HRESULT 并返回”这一整套动作。

仓库中这一模式使用得很彻底,以 src/server/Entrypoints.cpp 为例,一个启动函数里连续出现 5 处 RETURN_IF_WIN32_BOOL_FALSE,分别包裹 DuplicateHandleInitializeProcThreadAttributeList、两次 UpdateProcThreadAttributeCreateProcessW。全仓库范围内,RETURN_IF_WIN32_BOOL_FALSE/RETURN_IF_WIN32_ERROR 出现在 src/server/Entrypoints.cppsrc/winconpty/winconpty.cppsrc/host/srvinit.cpp 等 8 个文件中,是 console server 与 ConPTY 模块的标准防御姿态。

由此形成的固定模式(doc/WIL.md 明确描述):

  1. 函数内先声明所有资源,用 std::unique_ptrwil:: 智能指针/智能句柄保护;
  2. 每一次 Windows API 调用都跟一个 RETURN_IF_* 宏;
  3. 无论哪一步失败,RAII 保证资源全部正确清理,且函数以 HRESULT 作为返回码、用输出指针参数带回数据;
  4. 额外收益:任意位置的失败都会写入全局 tracing/debugging 通道,带精确行号与函数信息,可在调试器输出中直接查看。

若只想记录失败而不中断流程,每个 RETURN_IF_* 宏都有对应的 LOG_IF_* 版本:仅记录日志,函数继续执行。

4. 结果码约定:弃用 NTSTATUS,HRESULT 或异常优先

原文第四条规则是本文件最“反直觉”的一条:“The use of NTSTATUS as a result code is discouraged, HRESULT or exceptions are preferred. Functions should not return a status code if they would always return a successful status code. Any function that returns a status code should be marked noexcept and have the nodiscard attribute.”

拆开就是四条可执行的检查项:

  • NTSTATUS 不推荐作为结果码,优先 HRESULT,异常亦可。这与上一条“资源 + RETURN_IF_* + HRESULT + 输出参数”的模式互为表里:WIL 的宏体系围绕 HRESULT 工作,NTSTATUS 反而不在其惯用路径上。
  • 恒成功的函数不应返回状态码——永远返回 S_OK 的返回值是纯噪音,直接改为 void
  • 返回状态码的函数必须标注 noexcept——因为调用方拿到状态码后无法“抛出”异常路径来兜底,函数契约必须声明它不会抛。
  • 返回状态码的函数必须带 [[nodiscard]]——防止调用方漏检返回值。

这四条与 src/inc/CppCoreCheck/warnings.h 中的 WARNING_DECLARE_NOEXCEPT(26440,f.6 条目)互相印证:静态检查器会主动提示“可以声明 noexcept 的函数应当声明”,把风格约定落到了编译期。

5. C++/WinRT:强/弱引用与并发模型

原文第五条规则:“When contributing code in TerminalApp, be mindful to appropriately use C++/WinRT strong and weak references, and have a good understanding of C++/WinRT concurrency.”(在 TerminalApp 中贡献代码时,要恰当使用 C++/WinRT 的强引用与弱引用,并充分理解其并发模型。)

规则把适用范围限定在 TerminalApp 模块,因为那里是唯一深度绑定 WinRT 对象模型(激活对象、事件回调、async 协程)的 UI 层。它有两层含义:

弱引用防悬挂。 事件回调 lambda 若强持有被监听者(页面、控件、窗口),会形成“监听者不死、被监听者无法释放”的循环;但 lambda 捕获过期的强引用又会导致回调时崩溃。仓库的测试代码把这一风险讲得很直白,src/cascadia/LocalTests_TerminalApp/TabTests.cpp 中多处注释:

// the lambda. We'll crash trying to get a weak_ref to the TerminalPage

即测试刻意验证了“lambda 强引用页面、页面已销毁、回调里再取弱引用”这种错误组合会崩溃——反过来正是弱引用(winrt::weak_ref)被推荐的理由:lambda 捕获弱引用,回调触发时先尝试升级为强引用,失败则安全退出。

理解并发方案(concurrency)。 C++/WinRT 对象带有线程封送(concurrency scheme,如单线程封送 concurrency::single_threaded),跨线程调用需要 co_await 或显式封送。规则要求贡献者在写 TerminalApp 前先理解这套模型,否则极易引入跨线程访问单线程封送对象的隐性崩溃。从源码结构看,这也是仓库把该规则单列一条、且只针对 TerminalApp 的原因:src/cascadia/TerminalApp/ 是仓内唯一以 XAML + C++/WinRT 组织的大型模块(含 100 余个 resw 资源与 40 余个头/实现文件)。

三条规则如何协作:一个真实代码路径

把五条例规则放在一起,就能读懂仓库里一段典型代码的“语法选择”。以 src/winconpty/winconpty.cpp 定位 conhost 路径的静态函数为例:

  • static auto consoleHostPath = []() { ... } 用了 C++11 起的“magic statics”(注释原话 “Use the magic of magic statics to only calculate this once”)——线程安全的现代惯用法(规则 2);
  • wil::GetModuleFileNameW<std::wstring>wil::GetSystemDirectoryW<wil::unique_process_heap_string> 全程 WIL 类型,堆内存 RAII(规则 3);
  • 分支内调用 IsWow64Process2 后按返回的 BOOL 判断,遵循 Win32 约定;
  • 该文件同文件范围内出现 7 处 RETURN_IF_WIN32_BOOL_FALSE/RETURN_IF_WIN32_ERROR,保证任何一步失败都走 HRESULT + 日志路径(规则 3、4)。

对贡献者而言,写新代码时的自检顺序可以是:

  1. 我改的是既有代码块还是全新区域?→ 决定用“跟随上下文”还是“Modern C++”基准(规则 1、2);
  2. 是否触碰 Win32/NT API?→ 资源换 wil:: 类型,调用后挂 RETURN_IF_*/LOG_IF_*(规则 3);
  3. 返回状态码吗?→ 是否恒成功?若是改 void;否则补 noexcept + [[nodiscard]],并检查是否该用 HRESULT/异常而非 NTSTATUS(规则 4);
  4. 代码在 TerminalApp 里吗?→ 事件 lambda 用弱引用,跨线程访问先确认封送方案(规则 5);
  5. 最后用 AuditMode 配置(开启 CppCoreCheck 与 StaticAnalysis.ruleset)跑一遍静态分析,对照 src/inc/CppCoreCheck/warnings.h 的告警编号逐条收敛。

小结

doc/STYLE.md 五条规则构成了 Windows Terminal 仓库 C++ 层的完整风格闭环:局部修改服从历史惯例,整体演进锚定 C++ Core Guidelines 并由 AuditMode 静态检查(src/common.build.pre.props)强制执行;Win32/NT API 一律经 WIL 智能指针与 RETURN_IF_*/LOG_IF_* 宏体系(详见 doc/WIL.md)实现“资源 RAII + HRESULT + 失败自动落日志”的防御模式;状态码选择上弃 NTSTATUS 而取 HRESULT/异常,并要求 noexcept + nodiscard 双标注;C++/WinRT 侧则以弱引用与并发封送知识为 TerminalApp 模块设定准入门槛。掌握这份风格文档及其在源码中的落地证据,是向该仓库提交合规 C++ 代码的前提。

登录后查看全文
热门项目推荐
相关项目推荐