Skip to content
Draft
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
8 changes: 7 additions & 1 deletion src/rust/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...`
- **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`, `NullOrUndefined`, `Lenient`, `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. `Traced for Cell<T>` reads the `T` in place through `Cell::as_ptr()`, so a `trace` impl **MUST NOT** write into a cell (the same rule as for heap snapshot reporting, below)
- **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
- **Heap snapshots**: `#[jsg_resource(memory_info)]` + `impl jsg::VisitForMemoryInfo` (C++ `visitForMemoryInfo`); helper nodes implement `jsg::MemoryRetainer` and register with `jsg::jsg_memory_retainer!` (C++ `JSG_MEMORY_INFO`); names are `&'static CStr`. See `jsg-macros/README.md` §Heap Snapshots and `jsg/memory.rs`. Reporting code (`visit_for_memory_info`, `MemoryRetainer::memory_info`, any `MemoryInfo` impl) **MUST NOT**:
- **write into anything it can reach, above all a `Cell`** (no `Cell::set`/`replace`/`take`, no `RefCell::borrow_mut`, no counters or caches updated during a snapshot). `MemoryInfo for Cell<T>` reads the `T` in place through `Cell::as_ptr()`, as `Traced for Cell<T>` does, so a write into a cell whose value is being reported drops that value under a live reference: use-after-free from safe code. The compiler does not catch it, so review every new `memory_info`/`visit_for_memory_info` and `trace` impl for writes, including writes made through an `std::rc::Rc` back-reference;
- panic (it runs in a callback from C++, so a panic aborts the process);
- call into JavaScript.

A type in a cycle through `std::rc::Rc`/`Arc` must be a `MemoryRetainer`: only a retainer's deduplication ends the cycle, and any other type recurses until the stack overflows. When a collection or `Box` holds retainers inline (directly or in an `Option`/`Cell`/other inline wrapper), `MemoryInfo::self_bytes_reported` leaves their bytes to their own nodes; a hand-written impl for an inline wrapper must forward it
- **Rust JSG safety rules** (full text and examples in `src/rust/jsg/README.md` §Safety rules):
1. Never call a V8 API that takes a `v8::Local<v8::Context>` while an exception or a termination is pending: V8 either clears the exception on entry, so the script runs on, or fails a debug check and in a release build runs on with it pending. Every C++ shim that passes a context to V8 first checks `exceptionPending()` (`jsg/ffi.h`) and fails with a pending error. Never throw while execution is terminating, and never discard a `jsg::Error` from an operation that can fail with a pending error (a call, `Object::get`/`set`, a coercion, `ToJS::to_js`); pass it on with `?`. Creating strings, numbers, objects, arrays, error objects and templates is fine at any time.
2. Never put traced state (`TracedGlobal`, `jsg::Rc`, or a type holding them) behind `std::rc::Rc`. To give a callback or another party access, give it a `jsg::Rc` or `jsg::Weak` to the resource that owns the state (a method gets one as `this: jsg::Rc<Self>`). Clone a `jsg::Rc` right after moving it out of a traced field (`take`, `replace`, `pop`), since the moved one is neither strong nor traced ([#7976](https://github.com/cloudflare/workerd/issues/7976)). Don't put a `Global` in anything GC traces.
Expand Down
24 changes: 24 additions & 0 deletions src/rust/jsg-compile-tests/memory-info-names-are-static.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// V8 reads the names of heap snapshot edges after the tracker's callback returns, so an edge name
// is a `&'static CStr`, and one built at runtime is rejected.
use std::ffi::CString;

use jsg_macros::jsg_resource;

#[jsg_resource(memory_info)]
struct Holder {
name: String,
}

#[jsg_resource]
impl Holder {}

impl jsg::VisitForMemoryInfo for Holder {
fn visit_for_memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
let edge_name = CString::new("name").unwrap();
tracker.track_field(&edge_name, &self.name); //~ ERROR E0597
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// `#[jsg_resource(memory_info)]` reports the resource's fields through its
// `jsg::VisitForMemoryInfo` impl, so the resource must have one.
use jsg_macros::jsg_resource;

#[jsg_resource(memory_info)] //~ ERROR E0277
struct Holder {
name: String,
}

#[jsg_resource]
impl Holder {}
69 changes: 69 additions & 0 deletions src/rust/jsg-compile-tests/ok-memory-info-with-other-flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// `memory_info` combines with the other flags of `#[jsg_resource]`, in any order.
#![deny(warnings)]

use std::ffi::CStr;

use jsg_macros::jsg_resource;

#[jsg_resource(name = "Renamed", memory_info, custom_trace)]
pub struct Holder {
name: String,
}

#[jsg_resource]
impl Holder {}

impl jsg::Traced for Holder {}

impl jsg::VisitForMemoryInfo for Holder {
fn visit_for_memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"name", &self.name);
}
}

#[jsg_resource(custom_trace, memory_info)]
pub struct Other {
cache: Cache,
}

#[jsg_resource]
impl Other {}

impl jsg::Traced for Other {}

impl jsg::VisitForMemoryInfo for Other {
fn visit_for_memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"cache", &self.cache);
}
}

pub struct Cache {
entries: Vec<String>,
}

impl jsg::MemoryRetainer for Cache {
fn memory_name(&self) -> &'static CStr {
c"Cache"
}

fn memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"entries", &self.entries);
}
}

jsg::jsg_memory_retainer!(Cache);

pub fn t(name: String) -> (jsg::Rc<Holder>, jsg::Rc<Other>) {
(
jsg::Rc::new(Holder { name }),
jsg::Rc::new(Other {
cache: Cache {
entries: Vec::new(),
},
}),
)
}
5 changes: 5 additions & 0 deletions src/rust/jsg-cpp/ffi.c++
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ void cpp_ref_visit(GcVisitor* visitor, const CppRef& ref) {
ref.visitForGc(*::workerd::rust::jsg::gc_visitor_from_ffi(visitor));
}

void cpp_ref_track(
::workerd::rust::jsg::MemoryTracker& tracker, const char* edge_name, const CppRef& ref) {
ref.trackField(tracker, ::workerd::rust::jsg::memoryTrackerEdgeName(edge_name));
}

} // namespace workerd::rust::jsg_cpp
26 changes: 26 additions & 0 deletions src/rust/jsg-cpp/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod bridge {
unsafe extern "C++" {
include!("workerd/rust/jsg/ffi.h");
type GcVisitor = jsg::v8::ffi::GcVisitor;
type MemoryTracker = jsg::v8::ffi::MemoryTracker;
}

unsafe extern "C++" {
Expand All @@ -47,6 +48,18 @@ pub mod bridge {
///
/// `visitor` is the visitor of a visit in progress.
pub unsafe fn cpp_ref_visit(visitor: *mut GcVisitor, cpp_ref: &CppRef);
/// Reports the reference to a heap snapshot as C++ reports a `jsg::Ref` field, under
/// the edge `edge_name`.
///
/// # Safety
///
/// `edge_name` is a NUL-terminated string literal, or null for none: V8 reads it after
/// the snapshot's BuildEmbedderGraph callback returns.
pub unsafe fn cpp_ref_track(
tracker: Pin<&mut MemoryTracker>,
edge_name: *const c_char,
cpp_ref: &CppRef,
);
}

impl KjOwn<CppRef> {}
Expand Down Expand Up @@ -209,6 +222,19 @@ impl<T: CppResource> jsg::Traced for Ref<T> {
}
}

/// The C++ resource's node, as C++ reports a `jsg::Ref` field.
impl<T: CppResource> jsg::MemoryInfo for Ref<T> {
fn track_as_field(
&self,
tracker: &mut jsg::MemoryTracker<'_>,
edge_name: Option<&'static std::ffi::CStr>,
) {
let edge_name = edge_name.map_or(std::ptr::null(), std::ffi::CStr::as_ptr);
// SAFETY: the name is a literal or null.
unsafe { bridge::cpp_ref_track(tracker.as_ffi(), edge_name, &self.inner) };
}
}

impl<T: CppResource> jsg::Type for Ref<T> {
fn class_name() -> &'static str {
T::class_name()
Expand Down
13 changes: 13 additions & 0 deletions src/rust/jsg-cpp/jsg-cpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class CppRef final {
visitor.visit(ref);
}

void trackField(::workerd::jsg::MemoryTracker& tracker, kj::StringPtr edgeName) const {
tracker.trackField(edgeName, ref);
}

// For clone(): only CppRef can make the badge, so no one else can pair an object with a
// reference to a different one. Public only so that kj::heap can call it.
CppRef(kj::Badge<CppRef>, void* object, ::workerd::jsg::Ref<::workerd::jsg::Object> ref)
Expand All @@ -85,6 +89,9 @@ class CppRef final {
kj::Own<CppRef> cpp_ref_clone(const CppRef& ref);
size_t cpp_ref_object(const CppRef& ref);
void cpp_ref_visit(GcVisitor* visitor, const CppRef& ref);
// A null `edge_name` is the empty name C++ gives the elements of a container.
void cpp_ref_track(
::workerd::rust::jsg::MemoryTracker& tracker, const char* edge_name, const CppRef& ref);

// Runs `func(jsg::Lock&)` the way a JSG method callback runs C++ API code (jsg::liftKj()), and
// returns whether it completed. A `jsg::JsExceptionThrown` leaves its exception pending. A
Expand Down Expand Up @@ -228,6 +235,12 @@ class RustRef {
visitor.visit(wrapper);
}

// Reports the reference to a heap snapshot as an edge to the wrapper, whose
// `javascript_to_native` edge leads to the Rust resource's node.
void trackField(::workerd::jsg::MemoryTracker& tracker, kj::StringPtr edgeName) const {
tracker.trackField(edgeName, wrapper);
}

private:
explicit RustRef(::workerd::jsg::V8Ref<v8::Object> wrapper): wrapper(kj::mv(wrapper)) {}

Expand Down
74 changes: 73 additions & 1 deletion src/rust/jsg-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,9 @@ impl MyResource {
`#[jsg_resource]` on a struct synthesises:

- `impl jsg::Traced for MyType { fn trace(&self, visitor) { ... } }`
- `impl jsg::GarbageCollected for MyType { fn memory_name(&self) -> ... }`
- `impl jsg::GarbageCollected for MyType { fn memory_name(&self) -> ... }`, which with
`#[jsg_resource(memory_info)]` also reports the fields to heap snapshots (see
[Heap Snapshots](#heap-snapshots))

The generated `Traced::trace` body simply calls `Traced::trace` on **every named
field**:
Expand Down Expand Up @@ -590,3 +592,73 @@ impl jsg::Traced for DynamicResource {
```

`custom_trace` can be combined with `name`: `#[jsg_resource(name = "MyName", custom_trace)]`.

## Heap Snapshots

Every resource is a node in a V8 heap snapshot, as a C++ resource is: it is named after the
struct (`workerd / MyType`), sized as the struct plus its C++ `Wrappable`, and linked to its
JavaScript wrapper. To report its fields as well, as C++ `visitForMemoryInfo` does, add
`memory_info` to the struct's `#[jsg_resource]` and implement `jsg::VisitForMemoryInfo`. The
generated `GarbageCollected::memory_info` calls it; without `memory_info` a resource reports no
fields, and with it but without the impl the resource does not compile.

```rust
#[jsg_resource(memory_info)]
pub struct EventRouter {
handlers: HashMap<String, jsg::Rc<Handler>>,
on_error: RefCell<Option<jsg::v8::TracedGlobal<jsg::v8::Value>>>,
name: String,
}

impl jsg::VisitForMemoryInfo for EventRouter {
fn visit_for_memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"handlers", &self.handlers);
tracker.track_field(c"on_error", &self.on_error);
tracker.track_field(c"name", &self.name);
}
}
```

`tracker.track_field` takes any `jsg::MemoryInfo`, the counterpart of C++
`MemoryTracker::trackField`'s overloads: strings and collections are nodes of the memory they
allocate, a `jsg::Rc` is an edge to the resource's node, a `TracedGlobal` an edge into the V8 heap,
and a helper type that implements `jsg::MemoryRetainer` (C++ `JSG_MEMORY_INFO`) is a node of its
own. Edge and node names are `c"..."` literals, since V8 reads them after the tracker's callback
returns. See `src/rust/jsg/memory.rs` for the full list.

A helper type registers its `MemoryRetainer` impl with `jsg::jsg_memory_retainer!`, which
implements `jsg::MemoryInfo` for it, so that `track_field` takes it:

```rust
struct Cache {
entries: HashMap<String, String>,
}

impl jsg::MemoryRetainer for Cache {
fn memory_name(&self) -> &'static CStr {
c"Cache"
}

fn memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
tracker.track_field(c"entries", &self.entries);
}
}

jsg::jsg_memory_retainer!(Cache);
```

A retainer's node counts its own bytes, so a collection of retainers counts only its spare
capacity. A retainer reached twice, as through two `Rc`s, is one node, so a type in a cycle through
`std::rc::Rc` or `std::sync::Arc` must be a retainer, or reporting never ends. A retainer type is
`'static`.

The reporting code (`visit_for_memory_info`, `memory_info` and any `MemoryInfo` impl) follows the
rules in `jsg::MemoryInfo`'s docs:

- It must not panic. It runs in a callback from C++, where a panic aborts the process.
- It must not write into anything it can reach, a `Cell` above all. `Cell<T>` is reported in
place, through `Cell::as_ptr`, so a `set` or `replace` of a cell while its value is reported drops
the value under the reference. The compiler does not check this.
- It must not call into JavaScript.

`memory_info` can be combined with `name` and `custom_trace`.
23 changes: 20 additions & 3 deletions src/rust/jsg-macros/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::utils::elide_named_lifetimes;
use crate::utils::extract_name_attribute;
use crate::utils::extract_named_fields;
use crate::utils::has_custom_trace_flag;
use crate::utils::has_memory_info_flag;
use crate::utils::is_attr;
use crate::utils::is_lock_ref;
use crate::utils::is_result_type;
Expand All @@ -45,8 +46,10 @@ pub fn generate_resource_struct(attr: TokenStream, input: &syn::DeriveInput) ->
return err.to_compile_error().into();
}

// Check for `#[jsg_resource(custom_trace)]` before consuming `attr`.
// Check for `#[jsg_resource(custom_trace)]` and `#[jsg_resource(memory_info)]` before
// consuming `attr`.
let custom_trace = has_custom_trace_flag(&attr);
let memory_info = has_memory_info_flag(&attr);

// Clone the name before mutating `input` so we can borrow freely later.
let name = input.ident.clone();
Expand Down Expand Up @@ -81,6 +84,18 @@ pub fn generate_resource_struct(attr: TokenStream, input: &syn::DeriveInput) ->
}
};

// `memory_info` reports the fields through the user's `VisitForMemoryInfo` impl, as C++
// `JSG_RESOURCE_TYPE` calls a resource's `visitForMemoryInfo`.
let memory_info_fn = if memory_info {
quote! {
fn memory_info(&self, tracker: &mut jsg::MemoryTracker<'_>) {
<Self as jsg::VisitForMemoryInfo>::visit_for_memory_info(self, tracker);
}
}
} else {
quote! {}
};

let gc_impl = quote! {
#[automatically_derived]
impl jsg::GarbageCollected for #name {
Expand All @@ -93,6 +108,8 @@ pub fn generate_resource_struct(attr: TokenStream, input: &syn::DeriveInput) ->
::std::ffi::CStr::from_bytes_with_nul(concat!(#name_str, "\0").as_bytes())
.unwrap()
}

#memory_info_fn
}
};

Expand Down Expand Up @@ -481,8 +498,8 @@ fn parse_impl_args(attr: proc_macro2::TokenStream) -> syn::Result<ImplArgs> {
use syn::punctuated::Punctuated;

const EXPECTED: &str = "expected `custom_members` or `nested_types(...)` on a \
#[jsg_resource] impl block (`name` and `custom_trace` belong on \
the struct)";
#[jsg_resource] impl block (`name`, `custom_trace` and \
`memory_info` belong on the struct)";

let args = Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated.parse2(attr)?;
let mut custom_members = None;
Expand Down
Loading
Loading