File
Blob: docs/reference/rust-review-checklist.md
This document provides a checklist for reviewing Rust code in the workerd project. It covers style guidelines, common pitfalls, and critical patterns to watch for when ensuring code quality.
See ../../src/rust/jsg/README.md for additional context.
CXX Bridge Safety
All Rust/C++ interop uses the cxx crate. Each crate with a bridge declares
#[cxx::bridge(namespace = "workerd::rust::<crate>")] and has companion ffi.c++/ffi.h files.
- Always ensure that shared structs passed across the boundary are trivially safe to copy between languages.
- Always verify that shared types do not contain owning pointers, non-trivial destructors, or types whose layout differs between Rust and C++.
- Always verify that opaque types (
type Foo;in CXX bridge) are passed by reference orBox/UniquePtr. - Always verify that lifetimes are respected — an opaque C++ type behind
&Tmust outlive the Rust reference. - Always verify that bridges must use the
workerd::rust::<crate_name>naming convention. The only exception ispython-parserwhich usesedgeworker::rust::python_parser. - Always verify companion files. Every CXX bridge should have hand-written
ffi.c++andffi.himplementing the C++ side. Generated headers (<file>.rs.h) are produced by the build system.
Unsafe Code
Unsafe code is concentrated at FFI boundaries. Review every unsafe block and unsafe fn:
- Always verify that every
unsafeblock has a// SAFETY:comment explaining which invariants the caller is responsible for and why they hold at this call site. - Always verify that functions receiving raw pointers from C++ are declared
unsafe fn**. - Always verify that
unsafe impl Send/unsafe impl Syncare justified with a comment explaining why the type is safe to share across threads.jsg::Ref<T>is explicitly notSend(usesRc+UnsafeCellinternally) — flag any attempt to send it across threads.
Common unsafe patterns in this codebase:
- V8 handle operations:
Local::from_ffi()/into_ffi(), isolate pointer dereference. These are safe only when the isolate is locked and the handle scope is active. - Resource wrap/unwrap:
Ref::into_raw()leaks a ref-counted pointer asusize;Ref::from_raw()reconstructs it. These must be balanced — everyinto_rawneeds exactly onefrom_rawto avoid leaks or double-frees. - Trampoline closures: A closure is cast to
usizevia raw pointer, passed through CXX, and reconstructed on the other side. The closure must be consumed exactly once.
Error Handling
- Always ensure that library crates (e.g.,
dns,net,transpiler) usethiserrorfor domain-specific error types. These should implementDisplayand have descriptive error messages. - Always ensure that JSG-facing crates (e.g.,
api,jsg) usejsg::Errorwith anExceptionTypevariant (e.g.,TypeError,RangeError). Domain errors should implementFrom<DomainError> for jsg::Errorfor ergonomic?usage. - Never use
panic!for expected errors. Panics across FFI are undefined behavior. UseResultand propagate errors through the CXX bridge.unwrap()/expect()are acceptable in tests (clippy is configured withallow-unwrap-in-tests = true). - Error type changes are generally not breaking — same policy as the C++ side. Changing the
JS exception type (e.g., from generic error to
TypeError) is not normally a breaking change unless it removes properties that user code could depend on (e.g.,DOMExceptionhascodethatTypeErrordoes not).
JSG Resource Conventions
Rust types exposed to JavaScript via the JSG bindings follow these patterns:
_state: jsg::ResourceStateis a required field on all resource types. It holds the opaque pointers used by the C++ JSG layer to wrap/unwrap the Rust object.#[jsg_resource]on the impl block registers the type as a JS-visible resource.#[jsg_method]auto-converts Rustsnake_casemethod names to JavaScriptcamelCase. Methods with a receiver (&self/&mut self) are registered as instance methods on the prototype; methods without a receiver are registered as static methods on the constructor. Verify the converted name is correct and matches the intended API surface.#[jsg_static_constant]on aconstitem inside a#[jsg_resource]impl block exposes it as a read-only numeric constant on both the constructor and prototype (Rust equivalent ofJSG_STATIC_CONSTANT). The name is used as-is (no camelCase conversion).#[jsg_struct]is for value types (passed by value across the JS boundary).#[jsg_oneof]is for union/variant types (mapped from JS values by trying each variant).- Type mappings:
jsg::Numberwraps JS numbers (distinct fromf64).Vec<u8>maps toUint8Array, not a regular JSArray.Option<T>maps toT | undefined(rejectsnull).Nullable<T>maps toT | null | undefined.String/&strmap to JS strings. - GC tracing:
Ref<T>,Option<Ref<T>>, andNullable<Ref<T>>fields on#[jsg_resource]structs are automatically traced during GC.WeakRef<T>fields are not traced (no-op). Verify that any resource holding aRef<T>orNullable<Ref<T>>to another resource is properly traced — missing traces cause use-after-free when the child is collected prematurely.
Linting & Style
- Clippy runs with pedantic + nursery lint groups. Run
just clippy <crate>on all Rust changes. #[expect(clippy::...)]over#[allow(clippy::...)]—expectis stricter: it warns if the suppressed lint no longer fires, preventing stale suppressions from accumulating.- Import formatting: one
useper import line, grouped as std / external / crate (enforced byrustfmt.toml). Runjust formatto auto-fix.
Testing
- JSG test harness (
jsg_test::Harness): creates a V8 isolate, runs Rust code in a real V8 context viarun_in_context(|lock, ctx| { ... }). Used for testing JSG resources. - C++ KJ tests for Rust crates: some crates have companion
.c++test files usingkj_test(). These test the C++ side of the FFI bridge. - Inline
#[cfg(test)]modules: standard Rust unit tests for pure-Rust logic. - All test targets run with
RUST_BACKTRACE=1andRUST_TEST_THREADS=1(serial execution).
Review Checklist
When reviewing Rust code in workerd, check for each of these items.
- Never use magic numbers (Numeric literals) without explanation or named constants.
- Always use
#[must_use]. Functions returningResult,Option, or expensive-to-compute values that callers should not silently discard. - Never use
boolfunction parameters. Prefer an enum or options struct for clarity at call sites. E.g.,fn connect(secure: bool)should befn connect(mode: SecureMode)or take an options struct. - Always use
const fn/constfor compile-time evaluable functions or constants where appropriate. - Always avoid reinvented utility. Avoid custom code duplicating functionality already in the
jsgorkjRust crates, or in well-known ecosystem crates already vendored by the project. - Always avoid using
static mut. UseOnceLock,LazyLock, or other safe synchronization primitives where global state is genuinely needed. - Always flag missing
// SAFETY:comments onunsafe. - Always flag unjustified
unsafe impl Send/Syncuses. - Always flag unnecessary
.clone()/copies where borrowing or moving would suffice. - Always flag unnecessary use of
Stringwhere&strworks, orVec<T>where a slice would do. - Always flag locations where
#[allow(...)]is used where#[expect(...)]would work. Preferexpectto prevent stale suppressions. - Always ensure copyright header on new files, Every new
.rsfile must begin with the project copyright/license header using the current year. Expected format:
Flag any new file that uses a stale year (e.g.,// 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.02017-2022in a file created in 2026) or omits the header entirely.