Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions docs/reference/rust-review-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ Rust types exposed to JavaScript via the JSG bindings follow these patterns:
`v8::Local<v8::Uint8Array>`.
`Option<T>` maps to `T | undefined`; like `jsg::Optional<T>`, `null` converts as `T`.
`Nullable<T>` maps to `T | null` and, like `kj::Maybe<T>`, coerces `undefined` to `null` on
input, but a method argument is still required. Use `Option<Nullable<T>>` for an optional
`T | null | undefined` that keeps `undefined` distinct from `null`.
input, but a method argument is still required. Prefer `NullOrUndefined<T>` to
`Option<Nullable<T>>` for an optional `T | null | undefined`, with distinct `Some(T)`,
`Null`, and `Undefined` states. Omitted method arguments become `Undefined`. Use
`Lenient<T>` only when shallow type mismatches should also be accepted; converting it to
`NullOrUndefined<T>` maps `Unconvertable` to `Undefined`, while nested conversion errors
still propagate.
`String`/`&str` map to JS strings. Integers and `#[jsg_struct]`s convert like C++ JSG's
integers and dictionaries: out-of-range integers throw, and an absent struct field reads as
`undefined`.
Expand All @@ -118,10 +122,11 @@ Rust types exposed to JavaScript via the JSG bindings follow these patterns:
which GC never visits and which keeps its handle inline. A handle that a resource owns is a
`jsg::v8::TracedGlobal`, which GC traces so that a cycle through it can be collected, and which
keeps its handles in a box. `Global` has no `Traced` impl, so it cannot be a resource field.
- **GC tracing**: `Ref<T>`, `Option<Ref<T>>`, and `Nullable<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 a `Ref<T>` or `Nullable<Ref<T>>` to another resource is properly
traced — missing traces cause use-after-free when the child is collected prematurely.
- **GC tracing**: `jsg::Rc<T>` fields on `#[jsg_resource]` structs are automatically traced
during GC, including through `Option`, `Nullable`, and `NullOrUndefined` wrappers when
`Some`. `Lenient` traces both `Some` and `Unconvertable`. `jsg::Weak<T>` fields are not
traced (no-op). Verify that every resource reference is properly traced — missing traces
cause use-after-free when the child is collected prematurely.

### Rust JSG Safety Rules

Expand Down
3 changes: 2 additions & 1 deletion src/rust/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...`
- **CXX bridge**: `#[cxx::bridge(namespace = "workerd::rust::<crate>")]` with companion `ffi.c++`/`ffi.h` files
- **Namespace**: `workerd::rust::*` for crates in this directory, except `python-parser` → `edgeworker::rust::python_parser`; a crate living beside C++ uses that C++'s namespace (e.g. `workerd::server::cli`)
- **Errors**: `thiserror` for library crates; `jsg::Error` with `ExceptionType` for JSG-facing crates
- **JSG nullish values**: `Option<T>` is `T | undefined`; `Nullable<T>` is `T | null` (coerces `undefined` to `Null`, but method arguments remain required). Prefer `NullOrUndefined<T>` to `Option<Nullable<T>>` for three distinct `Some(T)`, `Null`, and `Undefined` states; omitted arguments become `Undefined`. `Lenient<T>` additionally preserves shallow mismatches as `Unconvertable`; converting it to `NullOrUndefined<T>` maps those to `Undefined`, and `Lenient::to_parts()` also returns the original JS value. See [Rust JSG nullish values](jsg/README.md#nullablet-and-nullorundefinedt).
- **JSG resources**: `#[jsg_resource]` on struct + impl block; `#[jsg_method]` auto-converts `snake_case` → `camelCase`; methods with `&self` become instance methods, methods without a receiver become static methods, and any other receiver is a compile error (JavaScript can re-enter a resource, so state that methods change goes in `Cell`/`RefCell` fields; never hold a `RefCell` borrow across a call that can run JavaScript, nor a `borrow_mut()` of one holding GC references across a call that can allocate on the V8 heap, wrap the resource or create or drop a `jsg::Rc` to it, since tracing skips a mutably borrowed `RefCell`); a method or accessor can take `this: jsg::Rc<Self>` as its first parameter instead of `&self` to get a `jsg::Rc` of its resource (like `JSG_THIS`) to hand to a callback; don't share the resource's state with a callback through an `std::rc::Rc` (see `jsg/README.md` §Reaching the resource from a callback); an eligible `#[jsg_method]` also gets a V8 Fast API callback (types in `jsg/fast_api.rs`) and must not call into V8; a `#[jsg_constructor]` returns `Self` or `Result<Self, E>` (an `Err` is thrown) and throws `TypeError` when called without `new`; `#[jsg_static_constant]` on `const` items exposes read-only numeric constants on both constructor and prototype (name kept as-is, no camelCase); `#[jsg_resource(nested_types(Foo, Name = Bar))]` on the impl block puts other resources' constructors on the prototype (`JSG_NESTED_TYPE`); resources integrate with GC via `Traced` + `GarbageCollected`: every named field is traced via `Traced::trace(&self.field, visitor)` and all non-traceable types use no-op `Traced` impls
- **Resources created by C++**: C++ creates a Rust resource through an `extern "Rust"` function of the crate that defines it, which returns the wrapper through `jsg::v8::run_ffi_callback`; C++ holds the wrapper (a `jsg::JsRef`), since it has no typed reference to a Rust resource, and checks a value's type through one that returns `jsg::resource::ffi_is::<R>`, a `kj::Maybe<bool>` that is none when an exception is pending. See `jsg/README.md` §Creating resources from C++.
- **Custom JSG registration**: `#[jsg_resource(custom_members)]` on an impl generates member factories but leaves `impl Resource` to the author. `Resource::members(flags)` uses ordinary Rust to select members, order, and property placement. Factories are `<rust_name>_member()` (constant names lowercased); ordinary property factories are `<getter_name>_member(PropertyPlacement)` (`Prototype`, `Instance` or `LazyInstance`) and include the paired setter. See `jsg-macros/README.md` §Custom member registration.
- **JSG properties**: three property macros on `#[jsg_resource]` impl blocks — `#[jsg_property(prototype|instance|lazy_instance [, name = "..."] [, readonly])]` (`prototype` registers an accessor on the prototype, equivalent to `JSG_PROTOTYPE_PROPERTY`; `instance` registers an own data property via V8 `SetNativeDataProperty`, equivalent to `JSG_INSTANCE_PROPERTY`; `lazy_instance` registers an own data property via V8 `SetLazyDataProperty` whose getter runs once, on first access, equivalent to `JSG_LAZY_INSTANCE_PROPERTY`: it has no setter, is writable unless `readonly` (`JSG_LAZY_READONLY_INSTANCE_PROPERTY`); declaration order sets `Object.keys()` order for automatic registration; `readonly` is a compile-time check preventing a paired setter; `name = "..."` overrides the JS name; prefer `prototype` in almost all cases), `#[jsg_inspect_property]` (registered under a unique symbol, invisible to normal enumeration and string-key lookup, surfaced by `node:util` `inspect()`, equivalent to `JSG_INSPECT_PROPERTY`), and `#[jsg_wildcard_property]` (answers string-named properties the object and its prototypes lack, at most one per type, equivalent to `JSG_WILDCARD_PROPERTY`; see `jsg/README.md`); setter auto-detected from `set_` prefix; read-only when no setter present; getter/setter `.length` and `.name` are set correctly when `spec_compliant_property_attributes` compat flag is enabled
- **`jsg::v8::Global` / `TracedGlobal`**: persistent handles. A `Global` is for handles Rust owns outside resources: GC never visits it, so it is always strong, holds its handle inline (no box), has no `Traced` impl (it cannot be a resource field), and is `Send` and `Sync`. A `TracedGlobal` is for resource fields, which GC traces to collect cycles; it is `Send` (not `Sync`). Either may be dropped on any thread. Under the isolate lock its strong handle is reset at once; without the lock it is queued and reset at the next `IsolateBase::Lock`, through `IsolateBase::applyDeferredActions()`; once the `Realm` is dropped (under the lock, before `Dispose`) the handles are left to `Dispose`. See `jsg/isolate_state.rs`. Hand-written FFI that resets a V8 handle directly must hold the lock. Rust never resets a `TracedGlobal`'s traced reference: every GC visit lets go of it and makes the strong handle strong again (`ClearWeak`) before C++ installs a new one if needed, and a drop just lets go of it; V8 frees untraced nodes at the next full GC. See `GcVisitor::visit_traced_global`. A `TracedGlobal` keeps its handles in a `Box<GlobalSlot>`, since V8 holds the address of a weak handle's slot; the slot of a `TracedGlobal` dropped after the `Realm` is kept until the isolate is disposed
- **JSG parameters**: function `.length` is the Web IDL required-argument count under `spec_compliant_property_attributes` (`this: jsg::Rc<Self>` and a leading `&mut Lock` are not counted; counting stops at the first type whose `FromJS::is_optional()` returns true), so put optional parameters last
- **`Traced`**: core tracing trait in `jsg::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::TracedGlobal`); `jsg::v8::Global` deliberately has none
- **`Traced`**: core tracing trait in `jsg::wrappable`; built-ins include no-op impls for primitives/value types and delegating impls for wrappers/collections (`Option`, `Nullable`, `NullOrUndefined`, `Lenient`, `Vec`, maps/sets, `Cell`, `jsg::Rc`, `jsg::Weak`, `jsg::v8::TracedGlobal`); `jsg::v8::Global` deliberately has none
- **Sequences to JavaScript**: `Vec<T>` converts to an `Array`, numbers included (`vec![1u8, 2]` makes `[1, 2]`). A typed array or an `ArrayBuffer` is an explicit conversion of a slice: `to_js_typed_array(lock)` makes the typed array of the element type, and `to_js_array_buffer(lock)` makes an `ArrayBuffer` of bytes (`[u8]` only), as C++ JSG returns for a `kj::Array<kj::byte>`. See `jsg/README.md` §Arrays, typed arrays and `ArrayBuffer`s
- **`#[jsg_resource(custom_trace)]`**: suppresses the auto-generated `Traced` impl so the user can write their own; `GarbageCollected` (`memory_name`), `jsg::Type`, `jsg::ToJS`, and `jsg::FromJS` are still generated
- **Rust JSG safety rules** (full text and examples in `src/rust/jsg/README.md` §Safety rules):
Expand Down
15 changes: 14 additions & 1 deletion src/rust/jsg-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ it, the fields are defined as data properties, an absent optional field as `unde
JavaScript runs; without it, they are set one by one, an absent optional field is omitted, and a
setter on `Object.prototype` runs, and what it throws is passed on.

Optional fields can use `Option<T>` (`None` is absent) or `jsg::NullOrUndefined<T>`
(`Undefined` is absent). Prefer `NullOrUndefined<T>` to `Option<jsg::Nullable<T>>`
when a field must distinguish `null` from `undefined`; its `Null` state always
produces a property whose value is `null`. An absent input property becomes `None`
or `Undefined`, respectively. If all public fields are optional, a `null` or
`undefined` input is accepted as an empty dictionary.

```rust
#[jsg_struct]
pub struct CaaRecord {
Expand Down Expand Up @@ -69,6 +76,11 @@ Generates a V8 `FunctionCallback` for a method on a `#[jsg_resource]` type.
- The first typed parameter may be `&mut Lock` / `&mut jsg::Lock` to receive the
isolate lock directly — it is not exposed as a JavaScript argument. In a method that takes
`this`, the lock comes right after `this`.
- `Option<T>`, `jsg::NullOrUndefined<T>`, and `jsg::Lenient<T>` arguments may be omitted.
Prefer `NullOrUndefined<T>` to `Option<jsg::Nullable<T>>` for three distinct
`Some(T)`, `Null`, and `Undefined` states; omission produces `Undefined`.
A `jsg::Nullable<T>` argument remains required, although explicitly passing
`undefined` converts to `Null`. See [nullish value conversions](../jsg/README.md#nullablet-and-nullorundefinedt).
- To take or return handles, name the callback's scope:
`fn f<'s>(&self, lock: &mut Lock<'s>, v: Local<'s, Value>) -> Local<'s, Value>`. The
generated callback ties `'s` to its own scope.
Expand Down Expand Up @@ -468,7 +480,8 @@ use no-op `Traced` impls; containers/wrappers recurse to inner values.
| `jsg::v8::TracedGlobal<T>` | Dual strong/traced — `visitor.visit_traced_global` (enables cycle collection) |
| `jsg::Weak<T>` | **Not traced** — does not keep the target alive |
| `jsg::v8::Global<T>` | **Not allowed** — no `Traced` impl; use `jsg::v8::TracedGlobal<T>` |
| `Option<T>` / `jsg::Nullable<T>` | Delegates to `T` when present |
| `Option<T>` / `jsg::Nullable<T>` / `jsg::NullOrUndefined<T>` | Delegates to `T` when `Some` |
| `jsg::Lenient<T>` | Traces `T` when `Some`, or the original JS value when `Unconvertable` |
| `Vec<T>`, `jsg::Dict<V>`, `HashMap<K,V>`, `BTreeMap<K,V>`, `HashSet<T>`, `BTreeSet<T>` | Delegates recursively to contained values |
| `Cell<T>` / `std::cell::Cell<T>` | Delegates via `as_ptr()` read (safe under single-threaded, non-reentrant GC tracing) |
| `RefCell<T>` | Delegates via `try_borrow()`; skipped while mutably borrowed (debug builds abort if `T` can hold GC references) |
Expand Down
54 changes: 31 additions & 23 deletions src/rust/jsg-macros/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use utils::extract_named_fields;
use utils::fast_api_signature;
use utils::is_lock_ref;
use utils::is_named_this;
use utils::is_null_or_undefined_type;
use utils::is_option_type;
use utils::is_rc_self;
use utils::is_result_type;
Expand All @@ -62,6 +63,8 @@ use utils::this_param;
/// Only `pub` fields are projected into the JavaScript object. Each converts with its `ToJS`
/// impl, and a field whose conversion fails fails the struct's conversion with that error.
/// The generated `Traced` impl visits every field, so a field can be a `jsg::Rc<R>`.
/// Optional fields use `Option<T>` or `jsg::NullOrUndefined<T>`; prefer the latter to
/// `Option<jsg::Nullable<T>>` to distinguish `null` from `undefined`.
/// Use `#[jsg_struct(name = "MyName")]` to override `Type::class_name()`
/// metadata (used in diagnostics/type reporting), not to define a JS class.
#[proc_macro_attribute]
Expand Down Expand Up @@ -92,10 +95,16 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
};
let field_name_str = field_name.to_string();
let field_type = &field.ty;
let optional = is_option_type(field_type);
all_optional &= optional;
let present = if is_option_type(field_type) {
Some(quote! { this.#field_name.is_some() })
} else if is_null_or_undefined_type(field_type) {
Some(quote! { !this.#field_name.is_undefined() })
} else {
None
};
all_optional &= present.is_some();

to_js_fields.push((field_name, field_name_str.clone(), optional));
to_js_fields.push((field_name, field_name_str.clone(), present));
// Like C++ JSG, an absent property reads as undefined.
field_extractions.push(quote! {
let prop = obj.get(lock, #field_name_str)?;
Expand All @@ -107,7 +116,7 @@ pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
// Like C++ JSG, null or undefined is an empty dictionary, which is an
// error if a field is required.
let empty = if all_optional {
quote! { return Ok(Self { #(#field_names: None),* }); }
quote! { return Ok(Self { #(#field_names: Default::default()),* }); }
} else {
quote! {
return Err(jsg::Error::new_type_error(format!(
Expand Down Expand Up @@ -195,31 +204,30 @@ fn struct_traced_impl(
}

/// The body of a `#[jsg_struct]`'s `ToJS::to_js`, given each public field's name, its
/// JavaScript name and whether it is an `Option`. Like C++ JSG's `JSG_STRUCT`, it follows the
/// `fast_jsg_struct` flag.
fn to_js_fn_body(fields: &[(&syn::Ident, String, bool)]) -> proc_macro2::TokenStream {
/// JavaScript name and the condition for its present state, if optional. Like C++ JSG's
/// `JSG_STRUCT`, it follows the `fast_jsg_struct` flag.
fn to_js_fn_body(
fields: &[(&syn::Ident, String, Option<proc_macro2::TokenStream>)],
) -> proc_macro2::TokenStream {
// A DictionaryTemplate instance has every field, so an absent optional field is undefined.
let data_properties = fields.iter().map(|(field_name, js_name, optional)| {
let value = if *optional {
quote! {
match this.#field_name {
Some(value) => jsg::ToJS::to_js(value, lock)?,
None => lock.undefined(),
}
}
} else {
quote! { jsg::ToJS::to_js(this.#field_name, lock)? }
};
quote! { properties.push((#js_name, #value)); }
let data_properties = fields.iter().map(|(field_name, js_name, _)| {
quote! {
properties.push((#js_name, jsg::ToJS::to_js(this.#field_name, lock)?));
}
});
// Set() skips an absent optional field rather than setting it to undefined.
let field_sets = fields.iter().map(|(field_name, js_name, optional)| {
let field_sets = fields.iter().map(|(field_name, js_name, present)| {
let set = quote! {
let value = jsg::ToJS::to_js(value, lock)?;
obj.set(lock, #js_name, value)?;
};
if *optional {
quote! { if let Some(value) = this.#field_name { #set } }
if let Some(present) = present {
quote! {
if #present {
let value = this.#field_name;
#set
}
}
} else {
quote! { { let value = this.#field_name; #set } }
}
Expand Down Expand Up @@ -528,7 +536,7 @@ fn unwrap_argument(
};
// Like C++ JSG (TypeWrapper::unwrap() in type-wrapper.h), a missing
// required argument or a top-level type mismatch is "Failed to execute
// ...". Only Option and Lenient arguments may be omitted.
// ...". FromJS::is_optional() determines whether an argument may be omitted.
quote! {
let not_of_type = || jsg::Error::new_type_error(format!(
"Failed to execute '{}' on '{}': parameter {} is not of type '{}'.",
Expand Down
14 changes: 14 additions & 0 deletions src/rust/jsg-macros/utils-test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ fn is_attr_cases() {
assert!(is_attr(&qualified.attrs[0], "jsg_method"));
}

#[test]
fn is_null_or_undefined_type_cases() {
assert!(is_null_or_undefined_type(&parse_quote!(
NullOrUndefined<String>
)));
assert!(is_null_or_undefined_type(&parse_quote!(
jsg::NullOrUndefined<u32>
)));
assert!(!is_null_or_undefined_type(&parse_quote!(String)));
assert!(!is_null_or_undefined_type(&parse_quote!(
&NullOrUndefined<String>
)));
}

#[test]
fn is_option_type_cases() {
assert!(is_option_type(&parse_quote!(Option<String>)));
Expand Down
6 changes: 6 additions & 0 deletions src/rust/jsg-macros/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ pub fn this_param(sig: &syn::Signature) -> Option<&syn::PatType> {
}
}

/// Returns true if `ty` is `NullOrUndefined<T>`.
pub fn is_null_or_undefined_type(ty: &syn::Type) -> bool {
matches!(ty, syn::Type::Path(p)
if p.path.segments.last().is_some_and(|s| s.ident == "NullOrUndefined"))
}

/// Returns true if `ty` is `Option<T>`.
pub fn is_option_type(ty: &syn::Type) -> bool {
matches!(ty, syn::Type::Path(p)
Expand Down
Loading
Loading