
workerd C 代码审查清单全解从 KJ 类型体系到并发安全的硬性规则【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd导读本文基于 review-checklist.mdworkerd 仓库内 C 代码审查的强制性检查清单系统展开 workerd 工程在内存管理、错误处理、Promise 生命周期、类型设计与代码风格上的全部硬性规则。这份清单是 workerdCloudflare Workers 的 JavaScript / Wasm 运行时在 V8 垃圾回收器与 KJ 事件循环双线程模型下长期沉淀的工程约束读者读完可以掌握一套可直接用于 code review 的逐项检查方法并理解每条规则背后的实现原理与仓库内对应的源码证据。一、为什么 workerd 需要一份专门的 C 审查清单workerd 是一个运行于 Linux 生产环境、同时构建于 macOS 与 Windows 的高并发运行时其核心架构由 V8 隔离isolate线程与 KJ 事件循环I/O 线程协同驱动还要承载 Capn Proto 零拷贝消息、tcmalloc 内存分配和跨平台抽象。在这种环境下普通 C 项目习以为常的写法如throw、裸指针、std::string拼接都可能演变成内存泄漏、悬垂引用或线程竞争。仓库中的 kj-style.md 明确要求detail/review-checklist.mdmustbe used when performing ANY code review of workerd C code即任何一次对 workerd C 代码的审查都必须逐项对照本清单。同时kj-style.md 还规定了一套“参考文件路由”机制涉及kj::Maybe/kj::OneOf/kj::str()/KJ_DEFER/KJ_SYSCALL/kj::downcast时须读 api-patterns.md涉及kj::Promise/.then()/.attach()/.eagerlyEvaluate()/协程/kj::MutexGuarded时须读 async-patterns.md设计新类、审查类层次或 constness 语义时须读 type-design.md。审查清单总计 36 条规则每条都带Always/Never/Avoid三个级别的强制语义。下文按主题归类逐条展开并补充对应的实现细节与仓库内证据。二、内存管理与所有权杜绝 STL 泄漏与裸指针1. Always检查 STL 类型泄漏Alwayscheck for STL leaking in:std::string,std::vector,std::optional,std::unique_ptr, etc.workerd 全面使用 KJ 类型替代 C 标准库容器这一映射关系在 kj-style.md 中有完整对照表不要用应该用std::stringkj::Stringowned/kj::StringPtrborrowedstd::vectorkj::ArrayT定长/kj::VectorT可增长std::unique_ptrkj::OwnTstd::shared_ptrkj::RcT/kj::ArcT线程安全std::optionalkj::MaybeTstd::functionkj::FunctionTstd::variantkj::OneOfT...std::span/ 数组引用kj::ArrayPtrTstd::exceptionkj::Exceptionstd::promise/std::futurekj::PromiseT/kj::ForkedPromiseTT*可空kj::MaybeT审查时注意头文件中禁止包含string、vector、memory、optional、functional、variant等标准库头文件源文件中只有在 KJ 无对应物或与第三方依赖交互时才允许使用std::。同时禁止FILE*、iostream与 C stdio一律使用kj::str()格式化、kj/io.h做 I/O。2. Never禁止裸new/deleteNeverallow rawnew/delete. Should bekj::heapT()or similar.KJ 的内存工厂函数族见 kj-style.md是唯一的堆分配入口kj::heapT(args...)→ 返回kj::OwnTkj::heapArrayT(size)→ 返回kj::ArrayTkj::heapArrayBuilderT(size)→ 逐元素构建数组kj::rcT(args...)→ 返回kj::RcTkj::arcT(args...)→ 返回kj::ArcT原则是需要可移动性时用堆分配不需要移动时优先用栈或成员变量拷贝语义优先用显式clone()方法而非拷贝构造函数。3. Never禁止可空裸指针Neveruse nullable raw pointers. Should bekj::MaybeT裸指针与引用在 KJ 语义中代表“不拥有、仅借用”且默认非空。可空性必须显式表达为kj::MaybeT。与之配套的解包规范来自 api-patterns.md// 正确KJ_IF_SOME 解包 KJ_IF_SOME(value, maybeValue) { use(value); // value 是对所含 T 的引用 } // 正确断言式解包 auto value KJ_ASSERT_NONNULL(maybeValue); // 若为 none 则断言失败 auto value KJ_REQUIRE_NONNULL(maybeValue); // 前置条件检查 auto value JSG_REQUIRE_NONNULL(maybeValue, ErrorType, message); // 带 JS 异常类型 // 错误直接解引用不存在这种用法 auto value *maybeValue; auto value maybeValue.value();当从kj::Maybe中移出值时必须用kj::mv()移出并将原 Maybe 置为kj::none避免悬垂引用KJ_IF_SOME(value, maybeValue) { auto movedValue kj::mv(value); maybeValue kj::none; use(movedValue); }4. 所有权模型与类型设计纵深补充清单第 8 条不混合接口与实现类与内存安全直接相关其底层依据是 type-design.md 的两类类型划分值类型value types可拷贝/移动、按值比较、可序列化无虚函数用模板实现多态必须有移动构造函数资源类型resource types不可拷贝、不可移动用kj::OwnT在堆上转移所有权用KJ_DISALLOW_COPY_AND_MOVE防止误拷贝按身份比较允许继承与虚函数。此外cpp-safety-review-checklist.md 进一步要求审查所有权与生命周期语义、kj::Own/kj::Rc/kj::Maybe的使用、RAII 与 CRTP 模式、析构函数正确性与清理顺序、缓冲区溢出与边界检查并考虑util/weak-refs.h等弱引用模式的适用性该文件位于 src/workerd/util/weak-refs.h。三、错误处理与异常规范1. Never禁止throw语句Neverusethrowstatements. Should useKJ_ASSERT/KJ_REQUIRE/KJ_FAIL_ASSERT/etcworkerd 的异常体系基于 KJ 断言宏定义于 KJ 的kj/debug.h由 kj-style.md 归纳为KJ_ASSERT(cond, msg, values...)—— 检查不变量本代码的 bugKJ_REQUIRE(cond, msg, values...)—— 检查前置条件调用方的 bugKJ_FAIL_ASSERT(msg, values...)—— 无条件失败KJ_SYSCALL(expr, msg, values...)—— 包装系统调用并检查返回值KJ_UNREACHABLE—— 标记不可达代码kj::throwFatalError(msg, values...)—— 抛出带堆栈跟踪的异常这些宏自动捕获文件/行号、字符串化操作数并生成堆栈跟踪。KJ 的理念是异常用于容错而非控制流异常代表“本不该发生”的事情bug、网络故障、资源耗尽如果调用方必须 catch 异常才能正常工作说明接口设计有误——应改用类似openIfExists()返回kj::Maybe的替代方案。2. Never禁止noexcept析构函数必须用noexcept(false)NeverusenoexceptAlwaysusenoexcept(false)on explicit destructors理由在 kj-style.md 中阐述得很清楚bug 可能在任何地方发生abort 永远不是正确选择。显式析构函数必须声明noexcept(false)并使用kj::UnwindDetector或恢复块处理“栈展开过程中再次展开”的问题。这一点在 cpp-safety-review-checklist.md 中被特别澄清为运行时特性workerd 遵循 KJ 的noexcept(false)析构约定审查时不应把它当成问题标记除非存在具体的异常安全缺陷例如栈展开时的双重异常。3. Never禁止手动 errno 检查Neveruse manual errno checks. Should useKJ_SYSCALL/KJ_SYSCALL_HANDLE_ERRORSapi-patterns.md 给出了标准写法int n; KJ_SYSCALL(n read(fd, buf, size)); // 出错时抛异常自动重试 EINTR KJ_SYSCALL_HANDLE_ERRORS(fd open(path, O_RDONLY)) { case ENOENT: return nullptr; // 处理特定错误码 default: KJ_FAIL_SYSCALL(open(), error); }KJ_SYSCALL会自动处理EINTR重试并包装错误信息KJ_SYSCALL_HANDLE_ERRORS允许对特定错误码做分支处理这是替代手写errno分支的唯一正规途径。四、Lambda 捕获与 Promise 生命周期审查的重灾区1. Never禁止[]全量值捕获Neveruse[]lambda captures[]会让生命周期分析在审查时完全失效。规则细化kj-style.mdNever用[]May用[]但仅限lambda 不会逃逸当前栈帧时——[]表示“该 lambda 不逃逸”对会逃逸的 lambda必须逐个显式捕获变量使用kj::coCapture(...)延长捕获变量的生命周期至 Promise 结束。cpp-safety-review-checklist.md 把“宽泛捕获”列为 MEDIUM 级安全问题传给.then()或延迟执行的 lambda 用[]或[this]而只需特定成员时应改为显式捕获并仔细考虑捕获变量的生命周期。仓库自带的 clang-tidy 检查workerd-unsafe-continuation-capture见 tools/clang-tidy/unsafe-continuation-capture.c会标记传给异步接收方kj/jsg::Promise::then/catch_、IoContext::run/addTask/awaitIo/addFunctor、kj::evalLater等却捕获裸引用、[this]或非拥有视图的 lambda。推荐的捕获模式来自 async-patterns.md场景捕获方式JSG 资源[self JSG_THIS]kj::Refcounted[self addRefToThis()]IoContextJS-lock 作用域内lambda 内auto context IoContext::current();IoContextJS-lock 作用域外[weakRef context.getWeakRef()]KJ_ASSERT_NONNULL(weakRef-tryGet())或weakRef-runIfAlive(...)链会喂给不透明包装器IILE 协程把this/context作为协程参数而非 lambda 捕获特别强调不要用kj::addRef(context)强引用 IoContext 来通过检查——被 IoContext 传递拥有的链会形成引用计数环。IoContext::WeakRefcontext.getWeakRef()返回见 src/workerd/io/io-context.h是引用计数、非拥有的 IoContext 句柄可在 IoContext 销毁后安全持有tryGet()返回MaybeIoContext上下文消失后变为kj::nonerunIfAlive(func)在上下文存活时执行func并返回是否执行。凡续体可能活过其起源上下文或运行在 JS-lock 作用域之外IoContext::current()可能取错或不存在时都应使用 WeakRef 模式。2. Always.attach()保证 Promise 存活期Alwaysverify proper use of.attach()on promises. Objects must stay alive for the promise durationasync-patterns.md 的经典正反例// 正确stream 存活到 readAllText() 完成 return stream-readAllText().attach(kj::mv(stream)); // 错误stream 立即销毁promise 持悬垂引用 auto promise stream-readAllText(); return promise;取消语义同样依赖 attach销毁kj::Promise会立即取消它续体不执行、只有析构函数运行需要用.attach(kj::defer(...))保证完成与取消两条路径都会执行清理。3. Always后台 Promise 必须.eagerlyEvaluate()Alwaysuse.eagerlyEvaluate()with background promises. Lazy continuations may never execute. Co-routines are eager by default.promise doWork().then([]() { KJ_LOG(INFO, done); // 不 wait/then 的话不会运行 }).eagerlyEvaluate([](kj::Exception e) { KJ_LOG(ERROR, e); // 错误处理器是必需的 });若使用协程eagerlyEvaluate()是隐式的无需显式调用。多个后台任务用kj::TaskSet统一管理并共享错误处理器。此外kj::evalNow()可将同步代码包成“异常即拒绝 Promise”的形式return kj::evalNow([]() { // 这里的 throw 变成拒绝的 Promise而不是同步异常 return doSomethingThatMightThrow(); });4. In-Flight I/O 缓冲区与悬垂续体纵深补充async-patterns.md 指出一个隐蔽陷阱异步 I/O 会写入一个它不拥有的缓冲区tryRead()接收裸void*所有者必须活得比操作久且取消必须在释放缓冲区之前拆除操作。只有 KJ 一侧能做到这一点.attach()、.then()捕获、协程帧局部变量、三参数IoContext::awaitIo。而传给jsg::Promise::then()的续体含IoContext::addFunctor()方式不行——它活在一个不透明的Wrappable里V8 GC 可能在操作仍在写入时回收它。正确做法是让 KJ 链持有缓冲区、只把读取的字节值传给续体auto promise kj::evalNow([]() - kj::Promisekj::Arraykj::byte { auto buffer kj::heapArraykj::byte(size); auto bytes buffer.asPtr(); return stream-tryRead(bytes.begin(), atLeast, bytes.size()) .then(buffer kj::mv(buffer) mutable { return buffer.first(amount).attach(kj::mv(buffer)); }); });若续体必须与在途操作共享缓冲区则用引用计数kj::Arc并把一个引用 attach 到 Promise 上。五、继承与类型设计接口即契约1. Never禁止混合接口类与实现类Nevermixed interface and implementation classes. No data members in interfaces, no non-final virtuals in implementations. Intermediate subclasses are oktype-design.md 给出的判据一个类要么是接口无数据成员、只有纯虚方法要么是实现无非 final 虚方法混用会引发脆弱的基类问题fragile base class。配套规则接口不应声明析构函数允许多重继承且鼓励通常继承多个接口实现继承可用于组合且不产生额外堆分配优先用kj::downcastTdebug 构建会断言而非static_cast做向下转型禁止dynamic_cast做多态分发用 if/else 长链转型派生类不可取应扩展基类接口dynamic_cast仅允许用于优化或诊断场景判据即使它永远返回 null代码仍正确。2. Avoid向下转型优先kj::downcastTAvoidstatic_castfor downcasting where possible. Should bekj::downcastT(debug-checked)auto derived kj::downcastDerivedType(baseRef); // debug 构建断言需要无 RTTI 的优化路径时用kj::dynamicDowncastIfAvailableT失败返回 null。3. 单例与全局状态Neveruse singletons or mutable globalsmain()或高层代码应显式构造组件并通过构造函数参数注入依赖。const方法必须线程安全可并发调用非const方法要求独占访问——kj::MutexGuardedT从类型上强制这一点.lockShared()返回const T.lockExclusive()返回T。同时禁止全局动态构造器静态/全局变量不得有动态构造函数全局constexpr常量没问题。六、代码风格与可读性让审查更快更稳1. Never禁止std::to_string与字符串拼接Neverusestd::to_stringorstring concatenation统一用kj::str()kj::String msg kj::str(count: , count, , name: , name);十六进制输出用kj::hex(n)可用KJ_STRINGIFY(MyType)或.toString()扩展literal_kj后缀生成可constexpr的kj::StringPtr。2. Never禁止/* */块注释Neveruse/* */block comments. Use//line comments文档注释放在声明之前注释不应陈述显而易见的内容应补充代码本身看不出的信息TODO 格式为// TODO(type): descriptiontype 取now/soon/someday/perf/security/cleanup/port/test其中TODO(now)必须在合入前处理。3. Always命名约定Alwaysverify naming convention conformance. TitleCase types, camelCase functions/variables, CAPS constantskj-style.md 的完整约定类别风格类型类、结构体TitleCase变量、函数、方法camelCase常量、枚举值CAPITAL_WITH_UNDERSCORES宏CAPITAL_WITH_UNDERSCORES带项目前缀KJ_、CAPNP_命名空间oneword保持简短私有命名空间用_文件module-name.c、module-name.h、module-name-test.c4. Always块必须带花括号Alwayscheck for missing braces around blocks. Required unless entire statement is on one line格式化细则还包括2 空格缩进、每行最长 100 字符、续行 4 空格缩进、if (foo)等关键字后加空格、函数名后不加空格foo(bar)、public:/private:/protected:相对类体反向缩进一个档位、命名空间内容不缩进、去除行尾空白。七、API 设计与健壮性1. Always避免裸bool参数Alwaysavoidboolfunction parameters. Preferenum classorWD_STRONG_BOOLfor clarity at call sites. E.g.,void connect(bool secure)should bevoid connect(SecureMode mode)workerd 为强类型布尔提供了WD_STRONG_BOOL宏定义于 src/workerd/util/strong-bool.h配套测试在 src/workerd/util/strong-bool-test.c。仓库中大量使用该宏例如 src/workerd/io/io-context.h 中的WD_STRONG_BOOL(IoContext_Runnable_Exceptional)以及 src/workerd/api/http.h、src/workerd/io/compatibility-date.h、src/workerd/io/io-channels.h 等文件。目的很直接调用点connect(SecureMode::YES)比connect(true)的意图明确得多且编译器能阻止隐式转换。2. Always错误码 / Maybe / 成功布尔用[[nodiscard]]Alwaysuse[[nodiscard]]with functions returning error codes,kj::Maybe, or success booleans that callers must checkcpp-safety-review-checklist.md 进一步给出 KJ 封装返回拥有型资源、kj::Maybe、错误指示或昂贵计算结果如kj::Promise的函数应加KJ_WARN_UNUSED_RESULT它可同时捕获两类问题静默丢弃调用方必须处理的错误码/Promise以及丢弃昂贵计算的结果。3. Always优先协程而非 Promise 链Alwaysprefer coroutines over kj::Promise chains. Nested.then()chains with complex error handling that would be more readable as a coroutine withco_await. Butalwaysavoid suggesting sweeping rewrites注意后半个限定审查建议应聚焦局部可读性提升不要提议大规模重写sweeping rewrites——这是 workerd 审查文化的明确要求。4. Always检查缺失的constexpr/constevalAlwayscheck for missingconstexpr/constevalwhere they would be appropriate全局constexpr常量是允许的且是唯一允许的全局对象形式。5. Always先查src/workerd/util/再提新抽象Alwaysavoid reinventing utility with custom code duplicating functionality already insrc/workerd/util/(e.g., custom ring buffer, small set, state machine, weak reference pattern).Alwayscheck the util directory before suggesting a new abstraction.从目录列表可见 src/workerd/util/ 已沉淀了大量通用设施环形缓冲 ring-buffer.h、状态机 state-machine.h、小型弱引用容器 small-weak-vector.h、弱引用模式 weak-refs.h、强布尔 strong-bool.h、取消器 canceler.h、批处理队列 batch-queue.h 等且大部分带有配套-test.c。审查者建议新抽象前必须先浏览该目录。6. Always检查缺失的overrideAlwayscheck for missingoverrideon virtual method overrides7. Always标记魔法数字Alwaysflag magic numbers (Numeric literals) without explanation or named constants八、新文件必须携带版权头Alwayscheck for copyright header on new files. Every new.cand.hfile must begin with the project copyright/license header using the current year (not copied from older files).标准格式// Copyright (c) current-year Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0任何新文件使用过期年份例如 2026 年创建却写2017-2022或完全省略头文件都必须被标记。九、纵深内存安全与线程安全专项审查清单之外workerd 还有一份配套的 cpp-safety-review-checklist.md用于内存安全、线程安全与并发正确性专项审查。它把容易出事的模式按严重度分级内存安全专项识别内存泄漏、use-after-free、悬垂指针/引用审查所有权语义与生命周期管理分析智能指针使用kj::Own、kj::Rc、kj::Maybe检查 RAII、CRTP 与析构顺序返回指向this或参数所拥有数据的非拥有视图引用、指针、kj::ArrayPtr、kj::StringPtr的方法必须标注KJ_LIFETIMEBOUND展开为[[clang::lifetimebound]]让编译器在视图超过借用对象存活期时告警捕获 GC 追踪引用jsg::RefT等的 lambda 应使用JSG_VISITABLE_LAMBDA见 src/workerd/jsg/function.h使 V8 GC 能穿透捕获追踪具有位置语义的 RAII 作用域守卫、锁等必须用KJ_DISALLOW_COPY_AND_MOVE防止意外移动破坏作用域不变量。线程安全与并发专项检查数据竞争、锁顺序与死锁、共享状态访问模式绝不允许 isolate 锁跨越协程挂起点jsg::Lock、Worker::Lock、jsg::V8StackScope等类型已标注KJ_DISALLOW_AS_COROUTINE_PARAM以在编译期强制禁止审查新锁/作用域类型时确认其同样带此标注捕获裸指针/引用的 RAII 对象不得跨越挂起点不安全使用kj I/O 对象绝不能被 V8 堆对象尤其是jsg::Object实例直接持有必须经IoOwn/IoPtr见 src/workerd/io/io-own.h保证生命周期与线程安全注意kj::Refcounted与kj::AtomicRefcounted的选择跨线程访问必须用原子引用计数。关键检测模式CRITICAL / HIGHV8 回调抛出 C 异常JSG 方法、属性 getter/setter 等 V8 回调若未用liftKj见 src/workerd/jsg/util.h包裹而抛出 C 异常——V8 回调必须捕获 C 异常并转换为 JS 异常V8 堆对象直接持有 kj I/O 对象jsg::Object子类以kj::OwnT/kj::RcT/kj::ArcT存储 I/O 层对象而未用IoOwn/IoPtr跨线程使用kj::RefcountedI/O 线程与 JS isolate 线程都可能访问的实例需要kj::AtomicRefcountedisolate 锁跨越co_await持有jsg::Lock、V8HandleScope等跨协程挂起点属于未定义行为jsg::Object子类间引用环两个及以上jsg::Object子类互持强引用jsg::RefT或kj::OwnT且未通过JSG_TRACE做 GC 追踪会形成 V8 GC 不可见的泄漏含 A→B→C→A 的传递环。MEDIUM 与运行时专项异步 lambda 宽泛捕获[]/[this]热路径中的隐式 GC 触发ArrayBuffer后备存储创建、字符串展平、v8::Object::New()DISALLOW_KJ_IO_DESTRUCTORS_SCOPE见 src/workerd/jsg/wrappable.h该作用域内销毁任何 KJ 异步/I/O 对象都会令进程崩溃强制 JS 堆对象使用IoOwn/IoPtrIoOwn析构时创建匹配的AllowAsyncDestructorsScope允许安全销毁。引入新 GC/析构路径时须验证作用域嵌套正确src/workerd/jsg/promise.h 与 src/workerd/io/io-context.c 中均有应用。运行时专项提醒workerd 使用 tcmalloc优化重点是减少分配次数而非单次分配大小勿建议换回标准 mallocCapn Proto 零拷贝消息不要“为安全起见”复制数据应使用遍历上限traversal limits防资源耗尽V8 在任何分配点都可能触发 GC注意 V8 分配与裸指针访问的交错生产运行于 Linux、同时构建于 macOS 与 Windows标记缺乏可移植替代的 Linux-only 假设如 epoll、/proc。十、落地把清单融入日常审查工作流这份清单不是悬空的原则而是与整个工程体系咬合的强制规范审查路由任何 workerd C 审查必读 review-checklist.md涉及 KJ 特定 API 时按 kj-style.md 的路由规则加载 api-patterns.md、async-patterns.md、type-design.md工具支撑仓库自带 clang-tidy 检查集tools/clang-tidy/workerd-lint.c覆盖清单中的多条规则如workerd-unsafe-continuation-capture对应 tools/clang-tidy/unsafe-continuation-capture.c、Promise 结果忽略检查tools/clang-tidy/promise-ignore-result.c、GC 访问检查tools/clang-tidy/visit-for-gc.c等每条都有正负向测试样例兜底原则清单自身也承认“not exhaustive”——它覆盖常见模式具体审查仍需结合代码上下文与工程判断见 cpp-safety-review-checklist.md 开头说明。将这份清单逐条内化为肌肉记忆是理解和审查 workerd 这类“V8 KJ 双线程 零拷贝 强类型抽象”复合型运行时 C 代码的最短路径。【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考