
SerenityOS 代码模式指南从 TRY/MUST 错误处理到容器选型的工程实践【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenitySerenityOS一个以 64 位 x86、Arm 与 RISC-V 为目标的图形化类 Unix 操作系统在长期演进中沉淀出了一整套贯穿内核Kernel与用户态Userland的 C 编码模式。本文以 Documentation/Patterns.md 为核心骨架逐条拆解TRY(...)/MUST(...)错误传播、Fallible 构造器、serenity_main程序入口、侵入式链表、AssertSize静态断言、字符串视图字面量、SourceLocation与四种数组容器选型并结合仓库中的真实源码与测试用例佐证其底层实现帮助你写出风格统一、健壮且符合 SerenityOS 社区审美的代码。引言为什么 SerenityOS 需要一套模式文档在大型 C/C 代码库中如果没有统一的约定同一类问题往往会被写成十几种风格迥异的解法。SerenityOS 的代码库覆盖内核、系统服务、图形界面、浏览器引擎与大量基础库参与贡献者众多因此文档 Documentation/Patterns.md 明确承担了追踪并描述反复出现的模式的职责这些模式一部分是在项目演进中自发涌现的另一部分则是有意采纳的目的是让新代码与既有代码保持一致并让好模式在代码库中进一步传播。与一般编码规范不同本文档关注的是具有实质技术收益的工程模式——它们大多与 SerenityOS 独特的错误处理哲学ErrorOr返回值和拒绝 OOM 恐慌的内核级健壮性要求深度绑定因此不能简单地照搬通用 C 最佳实践。TRY(...)基于ErrorOr的无样板错误传播模式动机SerenityOS 的错误处理并不依赖 C 异常而是通过AK::ErrorOrT返回值显式传播错误。AK/Error.h 中定义了[[nodiscard]]的ErrorOrT, E它内部是一个联合体要么持有值T要么持有ErrorType默认即AK::Error并额外用一个bool标记当前处于哪种状态。ErrorOrvoid, E则特化为持有AK::Empty的ErrorOrEmpty, E把无返回值建模为一种合法的值状态。如果每个可能失败的调用点都要手写if (result.is_error()) return result.release_error();代码会被样板淹没。TRY(...)宏正是为此而生它把执行 → 判错 → 提前返回压缩成一个表达式。宏的实现原理宏定义位于 AK/Try.h#define TRY(expression) \ ({ \ AK_IGNORE_DIAGNOSTIC(-Wshadow, \ auto _temporary_result (expression)); \ static_assert(!::AK::Detail::IsLvalueReferencedecltype(_temporary_result.release_value()), \ Do not return a reference from a fallible expression); \ if (_temporary_result.is_error()) [[unlikely]] \ return _temporary_result.release_error(); \ _temporary_result.release_value(); \ })逐行解读它借助 GCC/Clang 都支持的语句表达式statement expressions扩展让整个TRY(...)可以作为一个表达式嵌入赋值语句右侧用auto承接表达式结果配合AK_IGNORE_DIAGNOSTIC(-Wshadow)允许宏嵌套在另一个TRY内部再写TRY通过static_assert禁止从 fallible 表达式返回引用——因为语句表达式无论如何都会产生拷贝返回引用会得到悬垂引用这是宏作者刻意设计的编译期防线若结果处于错误态release_error()会从当前函数直接return因此TRY只能用在返回类型同样为ErrorOr或Error/Result的函数中若成功整个表达式的值即release_value()的结果。注意宏头部注释中的说明TRY面向任何具有预期 API 的结果类型主要以AK::Result与AK::Error为设计目标。而在内核态与用户态中错误载体通常都是ErrorOrT默认错误类型AK::Error。实战示例LibGfx 中的位图创建文档给出的第一个完整示例来自图形库Bitmap::create_shareable参见 AK/Try.h 的配套用法与 Userland/Libraries/LibGfx 目录#include AK/Try.h ErrorOrNonnullRefPtrBitmap Bitmap::create_shareable(BitmapFormat format, IntSize size, int scale_factor) { if (size_would_overflow(format, size, scale_factor)) return Error::from_string_literal(Gfx::Bitmap::create_shareable size overflow); auto const pitch minimum_pitch(size.width() * scale_factor, format); auto const data_size size_in_bytes(pitch, size.height() * scale_factor); auto buffer TRY(Core::AnonymousBuffer::create_with_size(round_up_to_power_of_two(data_size, PAGE_SIZE))); auto bitmap TRY(Bitmap::create_with_anonymous_buffer(format, buffer, size, scale_factor, {})); return bitmap; }这段代码展示了TRY的三种典型用法前置校验size_would_overflow不通过时直接return Error::from_string_literal(...)错误信息采用模块: 描述的字符串字面量风格连续传播匿名缓冲区创建失败、位图创建失败都会立即返回不再需要层层if返回值收尾最后一个表达式return bitmap;直接返回NonnullRefPtrBitmap它会被隐式转换为ErrorOrNonnullRefPtrBitmap的成功态。实战示例内核中的地址空间分配TRY同样贯穿内核内存管理代码文档引用了AddressSpace::allocate_region#include AK/Try.h ErrorOrRegion* AddressSpace::allocate_region(VirtualRange const range, StringView name, int prot, AllocationStrategy strategy) { VERIFY(range.is_valid()); OwnPtrKString region_name; if (!name.is_null()) region_name TRY(KString::try_create(name)); auto vmobject TRY(AnonymousVMObject::try_create_with_size(range.size(), strategy)); auto region TRY(Region::try_create_user_accessible(range, move(vmobject), 0, move(region_name), prot_to_region_access_flags(prot), MemoryType::Normal, false)); TRY(region-map(page_directory())); return add_region(move(region)); }值得注意的细节名称以try_开头的工厂函数KString::try_create、AnonymousVMObject::try_create_with_size、Region::try_create_user_accessible是可能失败的显式信号返回值通常是ErrorOrTTRY(region-map(page_directory()))用于传播ErrorOrvoid类型的错误——即使没有值需要取出判错返回依然成立此处不用异常、不记录错误栈错误类型在 AK/Error.h 中即AK::Error持有m_code、m_string_literal与m_syscall标志。与 Rust?运算符的类比TRY的行为与 Rust 中的?运算符几乎一致Rust 官方文档对?的描述是错误传播的快捷方式二者都做出错即从当前函数返回错误成功则解包出值。区别在于?是语言语法而TRY依赖编译器扩展且TRY的解包会强制产生一次移动/拷贝这也是它禁止返回引用的原因。MUST(...)语义明确的绝不允许失败与TRY的区别及使用纪律MUST(...)与TRY(...)结构几乎相同同样定义于 AK/Try.h唯一区别是当结果处于错误态时MUST调用VERIFY(!_temporary_result.is_error())直接断言崩溃而不是返回错误#define MUST(expression) \ ({ \ AK_IGNORE_DIAGNOSTIC(-Wshadow, \ auto _temporary_result (expression)); \ static_assert(!::AK::Detail::IsLvalueReferencedecltype(_temporary_result.release_value()), \ Do not return a reference from a fallible expression); \ VERIFY(!_temporary_result.is_error()); \ _temporary_result.release_value(); \ })文档给出了两条明确的使用纪律不要用MUST顶替暂时无法传播错误的TRY。此时应调用ErrorOr的release_value_but_fixme_should_propagate_errors()方法定义见 AK/Error.h取出值并用方法名中的FIXME标记未来改进点MUST仅用于两种场景要么通过其他途径已经证明宏内代码不可能失败要么失败后果严重到程序必须崩溃。示例先扩容后追加的循环#include AK/Vector.h ErrorOrvoid insert_one_to_onehundred(Vectorint vector) { TRY(vector.try_ensure_capacity(vector.size() 100)); for (int i 1; i 100; i) { // We previously made sure that we allocated enough space, so the append operation shouldnt ever fail. MUST(vector.try_append(i)); } return {}; }这里先用TRY确保容量足够容量不足时Vector的追加会因堆分配失败而报错随后循环内的try_append被MUST包裹因为容量已保证追加只会在桶内写入不可能失败若真失败则说明发生了更严重的逻辑错误断言崩溃是合理响应。错误构造工具的补充配合使用时会频繁见到 AK/Error.h 提供的错误构造方法Error::from_errno(int code)包装系统调用返回的 errno会VERIFY(code ! 0)Error::from_string_literal(char const ()[N])用户态专用直接书写静态错误信息如Class: Some failureError::from_string_view(StringView)按视图持有字符串为避免悬垂该函数对ByteString、String、FlyString等所有权类型做了 delete禁用临时字符串必须显式.view()才能传入Error::from_syscall(StringView syscall_name, int rc)记录失败的系统调用名与返回码Error::copy(Error const)显式拷贝错误。Fallible Constructors用静态工厂取代可失败构造函数为什么需要这个模式C 构造函数无法返回ErrorOrT而在 SerenityOS 中一切可能失败的操作内存分配、IO、解码都通过ErrorOr表达。若在构造函数体内失败只能靠异常或置位标志这与系统哲学相悖。因此约定需要执行可失败操作的类不提供会失败的构造函数而是定义名为create的静态工厂函数。模式结构create返回ErrorOrT或ErrorOrNonnullOwnPtrT它在内部完成两类工作为私有构造函数准备参数其中任何一步都可能TRY返回、在对象构造完成后执行仍需的可失败初始化真正的构造函数保持private只做纯成员初始化从而保证对象一旦构造出来就是有效的。文档示例解压器class Decompressor { public: static ErrorOrNonnullOwnPtrDecompressor create(NonnullOwnPtrCore::Stream::Stream stream) { auto buffer TRY(CircularBuffer::create_empty(32 * KiB)); auto decompressor TRY(adopt_nonnull_own_or_enomem(new (nothrow) Decompressor(move(stream), move(buffer)))); TRY(decompressor-initialize_settings_from_header()); return decompressor; } // ... snip ... private: Decompressor(NonnullOwnPtrCore::Stream::Stream stream, CircularBuffer buffer) : m_stream(move(stream)) , m_buffer(move(buffer)) { } CircularBuffer m_buffer; NonnullOwnPtrCore::Stream::Stream m_stream; }此例同时展示了三个模式的组合TRY(CircularBuffer::create_empty(32 * KiB))缓冲区创建失败即返回TRY(adopt_nonnull_own_or_enomem(new (nothrow) Decompressor(...)))new (nothrow)在分配失败时返回空指针而非抛异常随后由adopt_nonnull_own_or_enomem将其转换为ErrorOrNonnullOwnPtrDecompressorOOM 时返回 ENOMEM 错误TRY(decompressor-initialize_settings_from_header())对象构造完成后继续做可能失败的头解析初始化。注意new (nothrow)、adopt_nonnull_own_or_enomem、VERIFY这些原语广泛定义于 AK/OwnPtr.h、AK/Assertions.h 与 AK/NonnullOwnPtr.h 中读者可在 AK 目录下继续追读。serenity_main(...)SerenityOS 风格的程序入口模式动机SerenityOS 的程序不再暴露普通的 Cmain函数而是暴露serenity_main(Main::Arguments)。动机有二Main::Arguments以更贴近 Serenity API 的形式组织命令行参数返回ErrorOrint使入口函数可以直接用TRY(...)无缝传播错误省去大量 C 风格if (rc 0) return rc;样板。从 C main 到 serenity_main传统写法int main(int argc, char** argv) { return 0; }SerenityOS 写法#include LibMain/Main.h ErrorOrint serenity_main(Main::Arguments arguments) { return 0; }底层链接机制Main::Arguments与serenity_main的声明位于 Userland/Libraries/LibMain/Main.hnamespace Main { struct Arguments { int argc {}; char** argv {}; SpanStringView strings; }; int return_code_for_errors(); void set_return_code_for_errors(int); } ErrorOrint serenity_main(Main::Arguments);Arguments在保留argc/argv的同时额外提供了SpanStringView strings可直接按StringView遍历参数无需手工指针运算实现位于 Userland/Libraries/LibMain/Main.cpp库的构建配置见 Userland/Libraries/LibMain/CMakeLists.txt可执行程序链接LibMain后真正的 C 入口int main(int, char**)由库提供并在启动时调用serenity_main(...)错误码策略由Main::return_code_for_errors()/set_return_code_for_errors(int)控制——当serenity_main返回错误时LibMain会以此决定进程退出码。也就是说应用程序作者只需要写serenity_mainmain的样板与错误到退出码的转换全部由LibMain统一承担。这一模式的历史由来记录在 OS hacking: A better main() for SerenityOS C programs 视频中文档内引用此处不展开外部链接。Intrusive Lists面向 OOM 韧性的内核数据结构什么是侵入式链表文档引用 Intrusive linked lists 的定义当每个元素自身持有用于记录其在数据结构中归属的元数据对链表而言就是内嵌的节点对象时该数据结构就是侵入式的。与之相对Vector这样的非侵入式容器由容器自身分配存储。侵入式链表的核心收益是插入操作不进行任何内存分配因此插入绝不会因 OOM 失败错误处理代码可以大幅简化。这正是内核这种必须对 OOM 有韧性的环境所需要的。声明模式私有节点 公开类型别名通用约定是把侵入式链表节点作为私有成员存储再用公开类型别名把链表类型暴露给外部使用者。文档示例来自内核Region类相关代码位于 Kernel/Memory 目录class Region final : public WeakableRegion { public: // ... snip ... private: bool m_syscall_region : 1 { false }; IntrusiveListNodeRegion m_memory_manager_list_node; IntrusiveListNodeRegion m_vmobject_list_node; public: using ListInMemoryManager IntrusiveListRegion::m_memory_manager_list_node; using ListInVMObject IntrusiveListRegion::m_vmobject_list_node; };一个对象可以同时挂入多个链表m_memory_manager_list_node与m_vmobject_list_node是两个独立节点对应Region同时属于内存管理器维护的全局区域表与每个 VMObject 维护的区域表两种关系。使用方通过公开别名直接持有链表class MemoryManager { // ... snip ... Region::ListInMemoryManager m_kernel_regions; VectorUsedMemoryRange m_used_memory_ranges; VectorPhysicalMemoryRange m_physical_memory_ranges; VectorContiguousReservedMemoryRange m_reserved_memory_ranges; };底层实现侵入式链表的实现在 AK/IntrusiveList.hIntrusiveListNodeV, Container第 157 行起内嵌m_next/m_prev指针节点本身可感知所属容器IntrusiveList以非类型模板参数SubstitutedIntrusiveListNodeT, Container T::* member绑定节点在宿主类型中的成员指针因此同一个IntrusiveList类型天然知道自己遍历的是宿主的哪个成员node_to_value通过成员指针偏移从节点找回宿主对象IntrusiveListStorage持有m_first/m_last实现双向链表的头尾锚点。正是成员指针作为模板参数这一设计让Region::ListInMemoryManager与Region::ListInVMObject虽然是同一种节点类型的不同成员却能静态地区分开来。IntrusiveList在用户态的少数特定场景也会被使用但主体仍是内核代码。类型大小静态断言AK::AssertSize为什么普通 static_assert 不够好static_assert(sizeof(T) N)失败时编译器错误信息里只有你写下的期望值没有类型的真实大小开发者还得手动打印或用调试器查。SerenityOS 为此在 AK/StdLibExtraDetails.h 中引入了AssertSizetemplatetypename T, unsigned ExpectedSize, unsigned ActualSize struct __AssertSize : TrueType { static_assert(ActualSize ExpectedSize, actual size does not match expected size); consteval explicit operator bool() const { return value; } }; templatetypename T, unsigned ExpectedSize using AssertSize __AssertSizeT, ExpectedSize, sizeof(T);技巧在于真实大小sizeof(T)被放进了模板参数。当断言失败时编译器会把ExpectedSize与ActualSize两个具体数值一并打印在错误信息中作为模板实参的一部分省去手工排查。consteval explicit operator bool()让它可以写进static_assert(AssertSizeT, N());。使用示例与仓库中的真实调用#include AK/StdLibExtras.h struct Empty { }; static_assert(AssertSizeEmpty, 1());仓库中已有大量实际调用例如 AK/FloatingPoint.hstatic_assert(AssertSizef128, 16()); static_assert(AssertSizeFloatExtractorf128, sizeof(f128)()); static_assert(AssertSizeFloatExtractorf80, sizeof(f80)()); static_assert(AssertSizeFloatExtractorf64, sizeof(f64)()); static_assert(AssertSizeFloatExtractorf32, sizeof(f32)());这些断言保障了位级浮点提取器FloatExtractor的布局与预期一致——任何结构体布局意外变化都会在编译期被捕捉且错误信息直接给出实际多少字节。字符串视图字面量operatorsv零运行时开销的 StringView 构造AK::StringView支持 C17 引入的sv字符串字面量运算符定义于 AK/StringView.h[[nodiscard]] ALWAYS_INLINE consteval AK::StringView operatorsv(char const* cstring, size_t length) { return AK::StringView(cstring, length); }consteval保证编译期求值构造StringView时不需要在运行时用strlen扫描长度字面量本身位于二进制文件的只读数据段StringView只是指针 长度的轻量视图不拥有也不复制数据。用法与测试#include AK/String.h #include AK/StringView.h #include LibTest/TestCase.h TEST_CASE(string_view_literal_operator) { StringView literal_view foosv; String test_string foo; EXPECT_EQ(literal_view.length(), test_string.length()); EXPECT_EQ(literal_view, test_string); }示例中的TEST_CASE/EXPECT_EQ宏来自 Userland/Libraries/LibTest 测试框架说明了该模式在单元测试中的标准用法foosv与运行时构造的String在长度与内容上完全相等而前者无需动态分配。Source Location免预处理器宏的调用点捕获与 C20 std::source_location 的关系C20 的std::source_location允许以默认参数捕获调用者的文件 / 行号 / 函数名。AK/SourceLocation.h 提供了同名等价实现内部直接基于__builtin_FILE()、__builtin_LINE()、__builtin_FUNCTION()见SourceLocation::current()同时为AK::SourceLocation特化了AK::Formatter使其能直接以[\x1b[34m{}\x1b[0m {}:{}]的带色格式输出函数名 文件名:行号。#include AK/SourceLocation.h #include AK/StringView.h static StringView example_fn(const SourceLocation loc SourceLocation::current()) { return loc.function_name(); } int main(int, char**) { return example_fn().length(); }要点是SourceLocation::current()作为默认实参在调用点实例化因此example_fn()内部拿到的loc是调用者那一行的位置信息而不是example_fn自己的定义位置。这已成为 SerenityOS 添加调试插桩的惯用方式——不再需要把__FILE__/__LINE__塞进宏。按调试宏裁剪的别名技巧如果只想在某个调试宏开启时才真正捕获位置信息文档建议不要在带SourceLocation参数的函数里到处加#ifdef而是定义两个同名的类型别名/空类型#if LOCK_DEBUG # include AK/SourceLocation.h #endif #if LOCK_DEBUG using LockLocation SourceLocation; #else struct LockLocation { static constexpr LockLocation current() { return {}; } private: constexpr LockLocation() default; }; #endif当LOCK_DEBUG关闭时LockLocation退化为一个可被编译器完全优化掉的空结构体current()返回空对象所有调用点代码照常编译零运行时开销开启时则自动获得真实的调用位置。这样既保留了干净的调用方代码又实现了按配置裁剪调试信息。四种连续存储容器选型type[]、Array、Vector、FixedArray共同的视图搭档Spantype在讨论四种拥有数据的容器之前先明确Spantype的角色它不拥有数据只是对他人数据的视图。上述四种容器都能提供Span来观察全部或部分数据。凡是不关心容器种类、不需要调整大小的 API优先以Span作为参数类型——这与 AK/Span.h 中的定义一致也是 SerenityOS 中大量函数签名采用Span的原因。四者的定位对比容器存储方式是否可动态扩容典型用途C 风格数组type[]内联inline否仅用于实现其他集合或极特殊情况一般不鼓励直接使用指针长度风格同样被劝阻Arraytype, N内联否编译期已知大小的固定数组std::array的等价物零动态分配Vectortype内联 堆上外置存储是绝大多数列表场景的默认选择见 AK/Vector.hFixedArraytype堆上外置存储否运行期才知道大小、但初始化后不再变化仅在构造/析构时分配与释放逐条展开C 风格数组文档明确给出一般不建议的立场包括以指针 长度方式传递数组。它们仅作为其他集合的内部实现或特殊场景的兜底存在Arraystd::array的薄封装大小是模板参数的一部分数据内联分配从不做动态分配适合编译期常量大小的场景Vectorstd::vector的等价物可动态调整大小是基本列表需求的默认容器。其第二模板参数是可选的内联容量inline capacity数据优先放在内联缓冲中一旦超出内联容量就自动切换到堆上的外置存储并在扩容/缩容时自动搬移FixedArray本质是运行期定大小的Array。不能像Vector那样扩容但非常适合大小在编译期未知、初始化后不再变化的场景它保证除了构造函数与析构函数之外不做任何分配/释放——这对性能敏感路径与内核式资源纪律很有价值。选型决策建议尺寸编译期已知且固定 →Arraytype, N尺寸运行期才知道、但定下来就不变 →FixedArraytype需要随时增删、动态伸缩 →Vectortype只是想看看别人拥有的数据或作为函数参数 →Spantype除非在实现底层集合否则避免裸type[]与指针长度风格。总结模式如何共同构成 SerenityOS 的代码气质把文档中这些模式放在一起可以看到一条清晰的工程主线错误即值ErrorOrT承载一切可失败操作TRY/MUST让传播与断言各得其所release_value_but_fixme_should_propagate_errors()负责标记待改进点构造即有效Fallible 构造器模式create静态工厂 私有构造函数确保对象一旦创建就处于一致状态入口即框架serenity_mainLibMain把进程样板统一收敛OOM 韧性侵入式链表在必须对 OOM 稳健的环境内核中消除插入失败的路径Vector的try_ensure_capacity则把 OOM 显式化为可处理错误编译期把关AssertSize让布局错误在编译期暴露并给出真实数值operatorsv与consteval让字符串视图构造零运行时开销SourceLocation让调试插桩告别预处理器宏容器各司其职Array/Vector/FixedArray/Span的选型规则把内存生命周期这一最易出错的维度显式化。对想要为 SerenityOS 贡献代码、或仅仅是想学习如何在一个大型 C 代码库中建立统一且可扩展的错误处理纪律的开发者而言Documentation/Patterns.md 是一份值得反复对照的活文档而 AK、Kernel、Userland 中的源码与 Tests/AK 下的测试则是验证这些模式的最佳教材。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考