
Windows Terminal 异常处理边界在 C 代码库中封装异常、返回 HRESULT 的现代化实践【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal本文基于仓库中的 doc/EXCEPTIONS.md 展开系统讲解 Windows Terminal 及其前身 conhostWindows 控制台宿主在现代化改造过程中制定的 C 异常使用策略为什么异常对“老代码”是危险的、五条必须遵守的异常使用规则以及如何在类内部封装try/catch、用HRESULT与 WIL 非抛出设施替代异常抛出的真实源码级做法。读完后你将掌握一套可直接套用于大型遗留 C 代码库的“异常边界”工程方法。背景为什么异常对这个代码库是“危险物”doc/EXCEPTIONS.md 开篇的 Philosophy 一节交代了这条策略的历史根源控制台console最初是用 C 语言编写的彼时的 Windows 操作系统中 C 还相对冷门。作为 Windows 控制台现代化项目的一部分团队把代码库转换成了 C但出于“异常可能引入不可预期失败”的顾虑一直没有采用基于异常的报错方式。然而 STL 等标准库实在太有用很多时候直接使用它们会显著简化代码。于是团队制定了一套“在什么情况下可以引入异常”的规则。理解这个背景是理解所有规则的钥匙这是一个C 血统、以HRESULT/Win32 错误码为传统报错手段的代码库新写的 C/WinRT 代码如src/cascadia下的 Windows Terminal 应用层天然会抛出异常而src/host、src/buffer等老模块则以状态码方式处理错误。两套风格在同一个进程里共存异常如果“泄漏”到老代码的调用栈上就会越过那些从未准备捕获异常的老函数最终导致进程级崩溃——这正是规则一“不要让异常从新代码泄漏到老代码”存在的根本原因。五条异常使用规则doc/EXCEPTIONS.md 给出了五条明确规则以下逐条继承原文并结合仓库源码展开绝不允许DO NOT让异常从新代码泄漏进老代码应当DO适当使用NTSTATUS或HRESULT作为返回值推荐HRESULT应当DO把所有异常行为封装在实现类内部不要DO NOT往老代码里引入现代的抛异常代码。取而代之的做法是按需重构以允许封装或使用不依赖异常的代码应当DO使用 WILWindows Implementation Library作为“不抛异常的现代设施”的替代手段例如wil::unique_ptr。配套地doc/STYLE.md 在编码风格层面做了强化不建议使用NTSTATUS作为结果码优先HRESULT或异常返回状态码的函数应当标记noexcept并带有nodiscard属性——这与规则 1、2 形成呼应既然函数承诺不抛出且返回状态码调用方就“必须”检查返回值编译器可以帮你把关。规则一与规则三把 try/catch 关在类的边界内原文档给出了一个示例类展示异常行为如何在类内部消化、在类边界外以HRESULT形式呈现class ExceptionsDoNotLeak { public: HRESULT SomePublicFunction(); int iPublic; private: void _SomePrivateFunction(); int _iPrivate; };含义是私有成员函数_SomePrivateFunction()内部可以随意抛出异常哪怕内部用了 STL 容器、智能指针但公共入口SomePublicFunction()负责try/catch后向老代码世界返回HRESULT。对老代码来说这个类看起来和任何“返回状态码的 C 风格 API”没有区别。仓库中的真实代码正是这个模式的落地。以 ConPTY 连接启动流程 src/cascadia/TerminalConnection/ConptyConnection.cpp#L473-L494 为例_StartConnection内部连续使用 WIL 宏THROW_LAST_ERROR_IF_NULL(_hOutputThread)抛出异常但整个函数体被catch (...)兜住随后用wil::ResultFromCaughtException()把捕获到的异常转成HRESULT再根据hr的值如ERROR_DIRECTORY路径无效、ERROR_ELEVATION_REQUIRED需要提权拼装面向用户的错误信息。抛与捕获全部封装在ConptyConnection类内部异常从未越过这个类的边界。另一个更底层的例子是 WIL 失败上报回调 src/inc/WilErrorReporting.h#L16-L58ReportFailureToFallbackProvider被声明为noexcept并采用函数体try/catch结构C17 的noexcept try { } catch (...)语法即使内部写遥测日志失败也被静默吞掉——注释解释得很直白“我们只是在追踪时失败了还能去哪呢”同时该函数还特判了0x80131515这个HRESULT由于 C/WinRT 会把它作为异常抛出XAML 无障碍代码的要求它会被反复送达错误上报路径因此被识别为“非真实错误”直接跳过。这体现了规则一在错误处理设施自身上的自律错误处理器绝不能再成为异常的传播者。规则二HRESULT 优先NTSTATUS 退居其次“NTSTATUS或HRESULT作为返回值HRESULT优先”这条规则在 doc/STYLE.md 中进一步细化始终返回成功状态的函数不应返回状态码返回状态码的函数要noexcept[[nodiscard]]。源码中这套约定随处可见。例如 src/host/VtIo.cpp#L120-L123 中RETURN_IF_FAILED(_pPtySignalInputThread-Start()); } CATCH_RETURN();RETURN_IF_FAILED是 WIL 的结果处理宏一旦Start()返回非成功HRESULT立即以该值作为当前函数的返回值退出且沿途通过智能指针/句柄自动完成资源清理外层的CATCH_RETURN()宏与一个隐式try块配对把任何意外抛出的异常同样转化为返回值。这样即使被调用的新代码会抛异常VtIo对外仍然表现为“纯状态码函数”老调用方无需任何改动——这正是规则二与规则一协同工作的样子。统计可见THROW_IF_FAILED、RETURN_IF_FAILED等 WIL 宏在 src/host 与 src/cascadia 下被广泛使用如 src/cascadia/TerminalApp/Jumplist.cpp、src/cascadia/TerminalConnection/ConptyConnection.cpp 等文件中有大量调用点说明这套模式已全库推广而非零星试点。规则四老代码不加 throw靠重构隔离第四条规则的处理方式是“二选一”要么重构老代码把会抛异常的逻辑整体搬进一个实现类里、用状态码对外要么直接写非异常的 C 代码。这实际上把代码库切成了两种风格区src/cascadia下的 Windows Terminal 应用层是 C/WinRT 组件可以使用异常src/host等老模块保持状态码风格。两个区域的交界处如 src/host/exe/exemain.cpp、src/cascadia/TerminalApp/AppLogic.cpp 这类进程入口/装配点就是catch (...)HRESULT转换应当出现的位置。规则五用 WIL 的非抛出设施替代会抛异常的现代设施原文档在这一条下标注了 TODO仓库中由配套的 doc/WIL.md 补全了细节这里一并继承其要点非抛出的智能指针/句柄wil::resource.h提供了大量 OS 资源的智能句柄wil::unique_handle等离开作用域时自动调用匹配的释放函数如CloseHandle()wil::make_unique_nothrow()是std::make_unique的非抛出对应物返回wistd::unique_ptr而非std::unique_ptr可与既有无异常代码无缝集成。仓库中这类类型被大量使用例如 src/cascadia/TerminalConnection/ConptyConnection.h、src/buffer/out/textBuffer.hpp 等头文件里即可看到wil::unique_handle、wistd::unique_ptr成员。结果处理宏wil::result.h提供成体系的宏。典型场景是DuplicateHandle()这类“失败时返回FALSE、需再调GetLastError()”的 Win32 API——用RETURN_IF_WIN32_BOOL_FALSE包住调用即可自动完成这个模式失败时返回等价的HRESULT。由此形成的标准写法是函数开头用智能指针/句柄集中声明所有资源之后每个 Windows API 调用后跟一个RETURN_IF_*任何一点失败都能保证资源被正确清理前提是函数返回HRESULT、输出数据走指针参数。可观测性红利这些宏在失败时会自动把失败信息文件、行号、函数上下文写入全局 tracing/调试通道可在调试器输出中直接查看另有对应的LOG_IF_*宏变体只记录失败、不中断执行适合“失败可容忍”的分支。小结一个可复制的异常边界清单综合 doc/EXCEPTIONS.md 的五条规则与仓库实现可以提炼出在“C 血统 现代 C”混合代码库中引入异常时的检查清单先找边界确认异常发生位置到老代码调用方之间的所有帧要么整体封装进类要么整体重写为非抛出代码类外只认状态码公共 API 返回HRESULT标记noexcept与[[nodiscard]]见 doc/STYLE.md捕获用catch (...)wil::ResultFromCaughtException()不挑异常类型统一转码避免std::exception之外的异常逃逸参考 src/cascadia/TerminalConnection/ConptyConnection.cpp#L479-L494;资源管理交给 WIL 非抛出设施wil::unique_handle/wil::make_unique_nothrow配合RETURN_IF_*宏实现“声明资源—逐步检查—失败自动清理”保留日志钩子用LOG_IF_*处理可容忍失败用RETURN_IF_*处理致命失败让失败带文件行号进入 tracing 通道。这套策略的价值不在于“禁用异常”而在于把异常变成类内部的私有实现细节新代码可以自由享受 STL 与现代 C 的便利老代码看到的接口风格却与 20 年前写的那份 C 代码一样可预期。对于任何正在做遗留 C 代码库 C 化改造的团队doc/EXCEPTIONS.md 这份简短的“异常政策”及其在 src/host、src/cascadia 中的落地方式是一份值得直接借鉴的工程范本。【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考