Blob: src/rust/AGENTS.md
src/rust/
OVERVIEW
11 Rust library crates + 1 binary crate linked into workerd via CXX FFI. No Cargo workspace — entirely Bazel-driven (wd_rust_crate.bzl). Clippy pedantic+nursery enabled; allow-unwrap-in-tests.
CRATES
| Crate | Purpose |
|---|---|
jsg/ |
Rust JSG bindings: Lock, Rc<T>, Resource, Struct, Type, Realm, FeatureFlags, module registration; V8 handle types including typed arrays, ArrayBuffer, ArrayBufferView, SharedArrayBuffer, BackingStore |
jsg-macros/ |
Proc macros: #[jsg_struct], #[jsg_method], #[jsg_resource], #[jsg_oneof], #[jsg_static_constant], #[jsg_constructor] |
jsg-test/ |
Test harness (Harness) for JSG Rust bindings |
api/ |
Rust-implemented Node.js APIs; registers modules via register_nodejs_modules() |
dns/ |
DNS record parsing (CAA, NAPTR) via CXX bridge; legacy duplicate of api/dns.rs, pending removal |
net/ |
Single function: canonicalize_ip() |
kj/ |
Rust bindings for KJ library (http, io, own submodules); Result<T> = Result<T, cxx::KjError> |
cxx-integration/ |
Tokio runtime init; called from C++ main() before anything else |
transpiler/ |
TS type stripping via SWC (ts_strip(), StripOnly mode) |
python-parser/ |
Python import extraction via ruff_python_parser; namespace: edgeworker::rust:: |
gen-compile-cache/ |
Binary crate — V8 bytecode cache generator; calls C++ compile() via CXX |
CONVENTIONS
- CXX bridge:
#[cxx::bridge(namespace = "workerd::rust::<crate>")]with companionffi.c++/ffi.hfiles - Namespace: always
workerd::rust::*exceptpython-parser→edgeworker::rust::python_parser - Errors:
thiserrorfor library crates;jsg::ErrorwithExceptionTypefor JSG-facing crates - JSG resources:
#[jsg_resource]on struct + impl block;#[jsg_method]auto-convertssnake_case→camelCase; methods with&self/&mut selfbecome instance methods, methods without a receiver become static methods;#[jsg_static_constant]onconstitems exposes read-only numeric constants on both constructor and prototype (name kept as-is, no camelCase); resources integrate with GC viaTraced+GarbageCollected: every named field is traced viaTraced::trace(&self.field, visitor)and all non-traceable types use no-opTracedimpls - JSG properties: two property macros on
#[jsg_resource]impl blocks —#[jsg_property(prototype|instance [, name = "..."] [, readonly])](registers an accessor;prototypemaps toJSG_PROTOTYPE_PROPERTY,instancemaps toJSG_INSTANCE_PROPERTY;readonlyis a compile-time check preventing a paired setter;name = "..."overrides the JS name; preferprototypein almost all cases), and#[jsg_inspect_property](registered under a unique symbol, invisible to normal enumeration and string-key lookup, surfaced bynode:utilinspect(), equivalent toJSG_INSPECT_PROPERTY); setter auto-detected fromset_prefix; read-only when no setter present; getter/setter.lengthand.nameare set correctly whenspec_compliant_property_attributescompat flag is enabled Traced: core tracing trait injsg::wrappable; built-ins include no-op impls for primitives/value types and delegating impls for wrappers/collections (Option,Nullable,Vec, maps/sets,Cell,jsg::Rc,jsg::Weak,jsg::v8::Global)#[jsg_resource(custom_trace)]: suppresses the auto-generatedTracedimpl so the user can write their own;GarbageCollected(memory_name),jsg::Type,jsg::ToJS, andjsg::FromJSare still generated- Formatting:
rustfmt.toml—group_imports = "StdExternalCrate",imports_granularity = "Item"(oneuseper import) - Linting:
just clippy <crate>— pedantic+nursery;allow-unwrap-in-tests - Tests: inline
#[cfg(test)]modules; JSG tests usejsg_test::Harness::run_in_context(). Always run the fullsrc/rust/...test suite (bazel test //src/rust/...) rather than targeting a single crate — changes in shared crates likejsgorjsg-macroscan break downstream consumers - FFI pointers: functions receiving raw pointers must be
unsafe fn(seejsg/README.md) - Parameter ordering:
&Lock/&mut Lockmust always be the first parameter in any function that takes a lock (matching the C++ convention wherejsg::Lock&is always first). This applies to free functions, trait methods, and associated functions (excluding&self/&mut selfreceivers which come beforelock). - Method naming: do not use
get_prefixes on methods — e.g.buf.backing_store()notbuf.get_backing_store(). Static constructors belong on the marker struct (impl ArrayBuffer { fn new(...) }) not onimpl Local<'_, ArrayBuffer>. - FFI naming: instance methods on an existing handle use a
local_<type>_<method>prefix (e.g.local_array_buffer_byte_length). Static constructors that create a new value do not use thelocal_prefix — name them<type>_<method>(e.g.array_buffer_new_with_mode,array_buffer_maybe_new,backing_store_new_resizable). - FFI groups:
v8.rsmod ffi,ffi.h, andffi.c++all use matching comment groups (e.g.// Local<T>,// Local<Array>,// Local<TypedArray>,// Local<ArrayBuffer>,// Local<ArrayBufferView>,// Local<SharedArrayBuffer>,// BackingStore,// Unwrappers,// Global<T>,// FunctionCallbackInfo). When adding new FFI functions, place them in the correct group in all three files. Do not scatter related functions across groups. - Feature flags:
Lock::feature_flags()returns a capnpcompatibility_flags::Readerfor the current worker. Uselock.feature_flags().get_node_js_compat(). Flags are parsed once and stored in theRealmat construction; C++ passes canonical capnp bytes torealm_create(). Schema:src/workerd/io/compatibility-date.capnp, generated Rust bindings:compatibility_date_capnpcrate.
CXX BRIDGE: ASYNC AND ERROR HANDLING
C++ calling async Rust (extern "Rust")
Mark an extern "Rust" function async to generate a C++ function returning kj::Promise<T>. If the function borrows any references (including &self), it needs an explicit lifetime annotation, which in turn requires unsafe:
extern "Rust" {
// Borrows &self — needs explicit lifetime + unsafe.
async unsafe fn do_work<'a>(self: &'a MyType, arg: i32) -> Result<u64>;
// Only owned parameters — no lifetime or unsafe needed.
async fn do_work_owned(arg: i32) -> Result<u64>;
}Without the explicit lifetime, the CXX macro requires the future to be 'static, which fails if the async body references borrowed parameters.
Rust calling async C++ (extern "C++")
Mark an extern "C++" function async to wrap a C++ function returning kj::Promise<T> as a Rust Future. The Rust caller can .await it:
unsafe extern "C++" {
async fn request(
this_: Pin<&mut HttpService>,
method: HttpMethod,
url: &[u8],
) -> Result<()>;
}Note: this_ is used instead of self because CXX's self receiver syntax doesn't support Pin<&mut T>. This is needed for C++ types with virtual methods where the Rust side calls into a pinned C++ object.
Error handling across FFI
Functions returning Result<T> in extern "Rust" blocks translate to C++ functions that throw kj::Exception on error. The error type must implement std::error::Error (which provides IntoKjException). Recommended patterns:
thiserrorenums: Define a crate-levelErrorenum for structured errors. This is the preferred pattern for crates with multiple error cases.std::io::Error: Acceptable for purely I/O-related errors.cxx::KjError: UseKjError::new(KjExceptionType::Failed, message)when you need direct control over the KJ exception type.