From e6a4cf101961eb00fb3f94983bc6cdfe24e93dea Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 8 Jan 2026 15:57:16 -0500 Subject: [PATCH] add gc support to Rust owned objects --- build/wd_rust_crate.bzl | 10 +- src/rust/api/dns.rs | 22 +- src/rust/api/lib.rs | 26 +- src/rust/jsg-macros/README.md | 55 +- src/rust/jsg-macros/lib.rs | 326 +++++-- src/rust/jsg-test/ffi.c++ | 12 + src/rust/jsg-test/ffi.h | 3 + src/rust/jsg-test/lib.rs | 15 +- src/rust/jsg-test/tests/arrays.rs | 145 +-- src/rust/jsg-test/tests/gc.rs | 392 +++++++++ src/rust/jsg-test/tests/jsg_oneof.rs | 61 +- src/rust/jsg-test/tests/mod.rs | 1 + src/rust/jsg-test/tests/non_coercible.rs | 12 +- src/rust/jsg-test/tests/resource_callback.rs | 74 +- src/rust/jsg/README.md | 225 +++++ src/rust/jsg/ffi-inl.h | 45 + src/rust/jsg/ffi.c++ | 185 +++- src/rust/jsg/ffi.h | 93 +- src/rust/jsg/jsg.h | 15 +- src/rust/jsg/lib.rs | 319 ++----- src/rust/jsg/resource.rs | 367 ++++++++ src/rust/jsg/v8.rs | 875 ++++++++++++++++++- 22 files changed, 2681 insertions(+), 597 deletions(-) create mode 100644 src/rust/jsg-test/tests/gc.rs create mode 100644 src/rust/jsg/resource.rs diff --git a/build/wd_rust_crate.bzl b/build/wd_rust_crate.bzl index c63d0416db9..a6f9211f1d9 100644 --- a/build/wd_rust_crate.bzl +++ b/build/wd_rust_crate.bzl @@ -154,5 +154,13 @@ def wd_rust_crate( rust_unpretty( name = name + "@expand", deps = [":" + name], - tags = ["manual", "off-by-default"], + tags = ["manual"], + ) + + if len(test_proc_macro_deps) > 0: + rust_unpretty( + name = name + "_test@expand", + deps = [":" + name + "_test"], + tags = ["manual"], + testonly = True, ) diff --git a/src/rust/api/dns.rs b/src/rust/api/dns.rs index 8dd24f9e67f..c454b835376 100644 --- a/src/rust/api/dns.rs +++ b/src/rust/api/dns.rs @@ -1,4 +1,3 @@ -use jsg::ResourceState; use jsg_macros::jsg_method; use jsg_macros::jsg_resource; use jsg_macros::jsg_struct; @@ -110,9 +109,8 @@ pub fn parse_replacement(input: &[&str]) -> jsg::Result #[jsg_resource] pub struct DnsUtil { - // TODO(soon): Generated code. Move this to jsg-macros. - #[expect(clippy::pub_underscore_fields)] - pub _state: ResourceState, + #[expect(clippy::pub_underscore_fields)] // Placeholder until real fields are added + pub _unused: u32, } #[jsg_resource] @@ -264,9 +262,7 @@ mod tests { #[test] fn test_parse_caa_record_issue() { - let dns_util = DnsUtil { - _state: ResourceState::default(), - }; + let dns_util = DnsUtil { _unused: 0 }; let record = dns_util .parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67".to_owned()) .unwrap(); @@ -278,9 +274,7 @@ mod tests { #[test] fn test_parse_caa_record_issuewild() { - let dns_util = DnsUtil { - _state: ResourceState::default(), - }; + let dns_util = DnsUtil { _unused: 0 }; let record = dns_util .parse_caa_record( "\\# 21 00 09 69 73 73 75 65 77 69 6c 64 6c 65 74 73 65 6e 63 72 79 70 74" @@ -295,9 +289,7 @@ mod tests { #[test] fn test_parse_caa_record_invalid_field() { - let dns_util = DnsUtil { - _state: ResourceState::default(), - }; + let dns_util = DnsUtil { _unused: 0 }; let result = dns_util.parse_caa_record( "\\# 15 00 05 69 6e 76 61 6c 69 64 70 6b 69 2e 67 6f 6f 67".to_owned(), ); @@ -307,9 +299,7 @@ mod tests { #[test] fn test_parse_naptr_record() { - let dns_util = DnsUtil { - _state: ResourceState::default(), - }; + let dns_util = DnsUtil { _unused: 0 }; let record = dns_util .parse_naptr_record("\\# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00".to_owned()) .unwrap(); diff --git a/src/rust/api/lib.rs b/src/rust/api/lib.rs index 86e7e61f5c0..73bdfa0788d 100644 --- a/src/rust/api/lib.rs +++ b/src/rust/api/lib.rs @@ -1,10 +1,8 @@ use std::pin::Pin; -use jsg::ResourceState; -use jsg::ResourceTemplate; +use jsg::Resource; use crate::dns::DnsUtil; -use crate::dns::DnsUtilTemplate; pub mod dns; @@ -27,20 +25,15 @@ pub fn register_nodejs_modules(registry: Pin<&mut ffi::ModuleRegistry>) { "node-internal:dns", |isolate| unsafe { let mut lock = jsg::Lock::from_isolate_ptr(isolate); - let dns_util = jsg::Ref::new(DnsUtil { - _state: ResourceState::default(), - }); - let mut dns_util_template = DnsUtilTemplate::new(&mut lock); - - jsg::wrap_resource(&mut lock, dns_util, &mut dns_util_template).into_ffi() + let dns_util = DnsUtil::alloc(&mut lock, DnsUtil { _unused: 0 }); + DnsUtil::wrap(dns_util, &mut lock).into_ffi() }, - jsg::modules::ModuleType::INTERNAL, + jsg::modules::ModuleType::Internal, ); } #[cfg(test)] mod tests { - use jsg::ResourceTemplate; use jsg_test::Harness; use super::*; @@ -48,14 +41,11 @@ mod tests { #[test] fn test_wrap_resource_equality() { let harness = Harness::new(); - harness.run_in_context(|lock, _ctx| unsafe { - let dns_util = jsg::Ref::new(DnsUtil { - _state: ResourceState::default(), - }); - let mut dns_util_template = DnsUtilTemplate::new(lock); + harness.run_in_context(|lock, _ctx| { + let dns_util = DnsUtil::alloc(lock, DnsUtil { _unused: 0 }); - let lhs = jsg::wrap_resource(lock, dns_util.clone(), &mut dns_util_template); - let rhs = jsg::wrap_resource(lock, dns_util, &mut dns_util_template); + let lhs = DnsUtil::wrap(dns_util.clone(), lock); + let rhs = DnsUtil::wrap(dns_util, lock); assert_eq!(lhs, rhs); Ok(()) diff --git a/src/rust/jsg-macros/README.md b/src/rust/jsg-macros/README.md index a5880c92199..4a56c01e0fe 100644 --- a/src/rust/jsg-macros/README.md +++ b/src/rust/jsg-macros/README.md @@ -53,17 +53,15 @@ impl DnsUtil { Generates boilerplate for JSG resources. Applied to both struct definitions and impl blocks. Automatically implements `jsg::Type::class_name()` using the struct name, or a custom name if provided via the `name` parameter. -**Important:** Resource structs must include a `_state: jsg::ResourceState` field for internal JSG state management. - ```rust #[jsg_resource] pub struct DnsUtil { - pub _state: jsg::ResourceState, + pub _unused: u8, } #[jsg_resource(name = "CustomUtil")] pub struct MyUtil { - pub _state: jsg::ResourceState, + pub value: u32, } #[jsg_resource] @@ -75,7 +73,12 @@ impl DnsUtil { } ``` -On struct definitions, generates `jsg::Type`, wrapper struct, and `ResourceTemplate` implementations. On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation. +On struct definitions, generates: +- `jsg::Type` implementation +- `jsg::GarbageCollected` implementation (default, no-op trace) +- Wrapper struct and `ResourceTemplate` implementations + +On impl blocks, scans for `#[jsg_method]` attributes and generates the `Resource` trait implementation. ## `#[jsg_oneof]` @@ -105,3 +108,45 @@ impl MyResource { ``` The macro generates type-checking code that matches JavaScript values to enum variants without coercion. If no variant matches, a `TypeError` is thrown listing all expected types. + +### Garbage Collection + +Resources are automatically integrated with V8's cppgc garbage collector. The macro automatically generates a `GarbageCollected` implementation that traces fields requiring GC integration: + +- `Ref` fields - traces the underlying resource +- `TracedReference` fields - traces the JavaScript handle +- `Option>` and `Option>` - conditionally traces +- `RefCell>>` - supports cyclic references through interior mutability + +```rust +#[jsg_resource] +pub struct MyResource { + // Automatically traced + js_callback: Option>, + child_resource: Option>, + + // Not traced (plain data) + name: String, +} + +// Cyclic references using RefCell +#[jsg_resource] +pub struct Node { + name: String, + next: RefCell>>, +} +``` + +For complex cases or custom tracing logic, you can manually implement `GarbageCollected` without using the jsg_resource macro: + +```rust +pub struct CustomResource { + data: String, +} + +impl jsg::GarbageCollected for CustomResource { + fn trace(&self, visitor: &mut jsg::GcVisitor) { + // Custom tracing logic + } +} +``` diff --git a/src/rust/jsg-macros/lib.rs b/src/rust/jsg-macros/lib.rs index 86553c0ff72..5d0ca4b19d5 100644 --- a/src/rust/jsg-macros/lib.rs +++ b/src/rust/jsg-macros/lib.rs @@ -7,8 +7,8 @@ use syn::Fields; use syn::FnArg; use syn::ItemFn; use syn::ItemImpl; +use syn::Type; use syn::parse_macro_input; -use syn::spanned::Spanned; /// Generates `jsg::Struct` and `jsg::Type` implementations for data structures. /// @@ -188,7 +188,7 @@ pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut args = unsafe { jsg::v8::FunctionCallbackInfo::from_ffi(args) }; #(#unwraps)* let this = args.this(); - let self_ = jsg::unwrap_resource::(&mut lock, this); + let self_ = ::unwrap(&mut lock, this); let result = self_.#fn_name(#(#arg_exprs),*); #result_handling } @@ -200,6 +200,13 @@ pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream { /// /// On structs: generates `jsg::Type` and `ResourceTemplate`. /// On impl blocks: generates `Resource` trait with method registrations. +/// +/// The generated `GarbageCollected` implementation automatically traces fields that +/// need GC integration: +/// - `Ref` fields - traces the underlying resource +/// - `TracedReference` fields - traces the JavaScript handle +/// - `Option` where T is traceable - conditionally traces +/// - `RefCell>>` - supports cyclic references through interior mutability #[proc_macro_attribute] pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream { if let Ok(impl_block) = syn::parse::(item.clone()) { @@ -207,16 +214,56 @@ pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream { } let input = parse_macro_input!(item as DeriveInput); - let name = &input.ident; - let class_name = extract_name_attribute(&attr.to_string()).unwrap_or_else(|| name.to_string()); - let template_name = syn::Ident::new(&format!("{name}Template"), name.span()); + let name: &syn::Ident = &input.ident; - if !matches!(&input.data, Data::Struct(_)) { - return error( - &input, - "#[jsg_resource] can only be applied to structs or impl blocks", - ); - } + let class_name = if attr.is_empty() { + name.to_string() + } else { + extract_name_attribute(&attr.to_string()).unwrap_or_else(|| name.to_string()) + }; + + let template_name = template_name(name); + + // Extract fields for trace generation + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return syn::Error::new_spanned( + &input, + "#[jsg::resource] only supports structs with named fields", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned( + &input, + "#[jsg::resource] can only be applied to structs or impl blocks", + ) + .to_compile_error() + .into(); + } + }; + + // Generate trace statements for traceable fields + let trace_statements = generate_trace_statements(fields); + let gc_impl = if trace_statements.is_empty() { + quote! { + #[automatically_derived] + impl jsg::GarbageCollected for #name {} + } + } else { + quote! { + #[automatically_derived] + impl jsg::GarbageCollected for #name { + fn trace(&self, visitor: &mut jsg::GcVisitor) { + #(#trace_statements)* + } + } + } + }; quote! { #input @@ -249,6 +296,8 @@ pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream { } } + #gc_impl + #[automatically_derived] pub struct #template_name { pub constructor: jsg::v8::Global, @@ -268,66 +317,227 @@ pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream { - let self_ty = &impl_block.self_ty; +#[derive(Debug, Clone, Copy, PartialEq)] +enum TraceableType { + /// `Ref` - trace via `GarbageCollected` trait on the inner type + Ref, + /// `WeakRef` - weak reference, no tracing needed (doesn't keep alive) + WeakRef, + /// `TracedReference` - trace via `visitor.trace()` + TracedReference, + /// Not a traceable type + None, +} + +/// Checks if a type path matches a known traceable type. +/// Supports both unqualified (`Ref`) and qualified (`jsg::Ref`) paths. +fn get_traceable_type(ty: &Type) -> TraceableType { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + { + match segment.ident.to_string().as_str() { + "Ref" => return TraceableType::Ref, + "WeakRef" => return TraceableType::WeakRef, + "TracedReference" => return TraceableType::TracedReference, + _ => {} + } + } + TraceableType::None +} + +/// Extracts the inner type from `Option` or `std::option::Option` if present. +fn extract_option_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + && segment.ident == "Option" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner)) = args.args.first() + { + return Some(inner); + } + None +} - let method_registrations: Vec<_> = impl_block - .items +/// Extracts the inner type from `RefCell` or `std::cell::RefCell` if present. +fn extract_refcell_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + && segment.ident == "RefCell" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner)) = args.args.first() + { + return Some(inner); + } + None +} + +/// Generates trace statements for all traceable fields in a struct. +fn generate_trace_statements( + fields: &syn::punctuated::Punctuated, +) -> Vec { + fields .iter() - .filter_map(|item| { - let syn::ImplItem::Fn(method) = item else { - return None; - }; - let attr = method.attrs.iter().find(|a| { - a.path().is_ident("jsg") - || a.path() - .segments - .last() - .is_some_and(|s| s.ident == "jsg_method") - })?; - - let rust_name = &method.sig.ident; - let js_name = extract_name_attribute(&attr.meta.to_token_stream().to_string()) - .unwrap_or_else(|| snake_to_camel(&rust_name.to_string())); - let callback = syn::Ident::new(&format!("{rust_name}_callback"), rust_name.span()); - - Some(quote! { - jsg::Member::Method { name: #js_name.to_owned(), callback: Self::#callback } - }) + .filter_map(|field| { + let field_name = field.ident.as_ref()?; + let ty = &field.ty; + + // Check if it's RefCell<...> wrapping a traceable type + if let Some(refcell_inner) = extract_refcell_inner(ty) { + // RefCell> + if let Some(option_inner) = extract_option_inner(refcell_inner) { + match get_traceable_type(option_inner) { + TraceableType::Ref => { + return Some(quote! { + if let Some(ref inner) = *self.#field_name.borrow() { + visitor.visit_ref(inner); + } + }); + } + TraceableType::WeakRef => { + return Some(quote! { + if let Some(ref inner) = *self.#field_name.borrow() { + inner.trace(visitor); + } + }); + } + TraceableType::TracedReference => { + return Some(quote! { + if let Some(ref inner) = *self.#field_name.borrow() { + visitor.trace(inner); + } + }); + } + TraceableType::None => {} + } + } + + // RefCell (without Option) + match get_traceable_type(refcell_inner) { + TraceableType::Ref => { + return Some(quote! { + visitor.visit_ref(&*self.#field_name.borrow()); + }); + } + TraceableType::WeakRef => { + return Some(quote! { + self.#field_name.borrow().trace(visitor); + }); + } + TraceableType::TracedReference => { + return Some(quote! { + visitor.trace(&*self.#field_name.borrow()); + }); + } + TraceableType::None => {} + } + } + + // Check if it's Option + if let Some(inner_ty) = extract_option_inner(ty) { + match get_traceable_type(inner_ty) { + TraceableType::Ref => { + return Some(quote! { + if let Some(ref inner) = self.#field_name { + visitor.visit_ref(inner); + } + }); + } + TraceableType::WeakRef => { + return Some(quote! { + if let Some(ref inner) = self.#field_name { + inner.trace(visitor); + } + }); + } + TraceableType::TracedReference => { + return Some(quote! { + if let Some(ref inner) = self.#field_name { + visitor.trace(inner); + } + }); + } + TraceableType::None => {} + } + } + + match get_traceable_type(ty) { + TraceableType::Ref => Some(quote! { + visitor.visit_ref(&self.#field_name); + }), + TraceableType::WeakRef => Some(quote! { + self.#field_name.trace(visitor); + }), + TraceableType::TracedReference => Some(quote! { + visitor.trace(&self.#field_name); + }), + TraceableType::None => None, + } }) - .collect(); + .collect() +} + +fn template_name(name: &syn::Ident) -> syn::Ident { + syn::Ident::new(&format!("{name}Template"), name.span()) +} + +fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream { + let self_ty = &impl_block.self_ty; + let mut method_registrations = Vec::new(); + + for item in &impl_block.items { + if let syn::ImplItem::Fn(method) = item { + for attr in &method.attrs { + if attr.path().is_ident("jsg") + || (attr.path().segments.len() == 1 + && attr.path().segments[0].ident == "jsg_method") + { + let rust_method_name = &method.sig.ident; + + let js_name = extract_name_attribute(&attr.meta.to_token_stream().to_string()) + .unwrap_or_else(|| snake_to_camel(&rust_method_name.to_string())); + let callback_name = syn::Ident::new( + &format!("{rust_method_name}_callback"), + rust_method_name.span(), + ); + + method_registrations.push(quote! { + jsg::Member::Method { + name: #js_name.to_owned(), + callback: Self::#callback_name, + } + }); + } + } + } + } let type_name = match &**self_ty { - syn::Type::Path(p) => p - .path - .segments - .last() - .map_or("Unknown", |s| s.ident.to_string().leak()), - _ => "Unknown", + syn::Type::Path(type_path) => { + &type_path + .path + .segments + .last() + .expect("Type path must have at least one segment") + .ident + } + _ => todo!(), }; - let drop_fn = syn::Ident::new(&format!("drop_{type_name}"), self_ty.span()); + let template_name = template_name(type_name); quote! { #impl_block - #[allow(non_snake_case)] - #[automatically_derived] - unsafe extern "C" fn #drop_fn(isolate: *mut jsg::v8::ffi::Isolate, this: *mut std::os::raw::c_void) { - jsg::drop_resource::<#self_ty>(isolate, this); - } - #[automatically_derived] impl jsg::Resource for #self_ty { - fn members() -> Vec where Self: Sized { - vec![#(#method_registrations,)*] - } + type Template = #template_name; - fn get_drop_fn(&self) -> unsafe extern "C" fn(*mut jsg::v8::ffi::Isolate, *mut std::os::raw::c_void) { - #drop_fn - } - - fn get_state(&mut self) -> &mut jsg::ResourceState { - &mut self._state + fn members() -> Vec + where + Self: Sized, + { + vec![ + #(#method_registrations,)* + ] } } } diff --git a/src/rust/jsg-test/ffi.c++ b/src/rust/jsg-test/ffi.c++ index a65016fcfe1..1a6ab799871 100644 --- a/src/rust/jsg-test/ffi.c++ +++ b/src/rust/jsg-test/ffi.c++ @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -99,6 +100,17 @@ void TestHarness::run_in_context( }); } +void request_gc(Isolate* isolate) { + // Request V8 garbage collection + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); + + // Also explicitly trigger cppgc collection for the CppHeap + if (auto* cppHeap = isolate->GetCppHeap()) { + cppHeap->CollectGarbageForTesting(cppgc::EmbedderStackState::kNoHeapPointers); + } +} + } // namespace rust::jsg_test } // namespace workerd diff --git a/src/rust/jsg-test/ffi.h b/src/rust/jsg-test/ffi.h index d3b3c6cec78..9016b276d41 100644 --- a/src/rust/jsg-test/ffi.h +++ b/src/rust/jsg-test/ffi.h @@ -50,6 +50,9 @@ class TestHarness { kj::Own create_test_harness(); +// Triggers a full garbage collection for testing purposes. +void request_gc(Isolate* isolate); + } // namespace rust::jsg_test } // namespace workerd diff --git a/src/rust/jsg-test/lib.rs b/src/rust/jsg-test/lib.rs index 4622a36d9e8..8cdd82742cd 100644 --- a/src/rust/jsg-test/lib.rs +++ b/src/rust/jsg-test/lib.rs @@ -12,7 +12,6 @@ mod ffi { #[namespace = "workerd::rust::jsg"] unsafe extern "C++" { include!("workerd/rust/jsg/ffi.h"); - type Isolate = jsg::v8::ffi::Isolate; type Local = jsg::v8::ffi::Local; } @@ -25,7 +24,6 @@ mod ffi { unsafe extern "C++" { include!("workerd/rust/jsg-test/ffi.h"); - type TestHarness; type EvalContext; @@ -38,6 +36,13 @@ mod ffi { pub unsafe fn eval(self: &EvalContext, code: &str) -> EvalResult; pub unsafe fn set_global(self: &EvalContext, name: &str, value: Local); + + /// Triggers a full garbage collection for testing purposes. + /// Note: For GC to actually collect objects, they must not be reachable from the + /// current HandleScope. + #[expect(clippy::allow_attributes)] // Only used in tests, but #[expect(dead_code)] fails during test builds + #[allow(dead_code)] + pub unsafe fn request_gc(isolate: *mut Isolate); } } @@ -156,6 +161,12 @@ impl Harness { .run_in_context(&raw mut callback as usize, trampoline::); } } + + pub fn request_gc(lock: &mut jsg::Lock) { + unsafe { + ffi::request_gc(lock.isolate().as_ffi()); + } + } } impl Default for Harness { diff --git a/src/rust/jsg-test/tests/arrays.rs b/src/rust/jsg-test/tests/arrays.rs index 64d90c65330..6cc1f40a834 100644 --- a/src/rust/jsg-test/tests/arrays.rs +++ b/src/rust/jsg-test/tests/arrays.rs @@ -1,6 +1,5 @@ use jsg::Number; -use jsg::ResourceState; -use jsg::ResourceTemplate; +use jsg::Resource; use jsg::ToJS; use jsg_macros::jsg_method; use jsg_macros::jsg_resource; @@ -14,7 +13,7 @@ struct Person { #[jsg_resource] struct ArrayResource { - _state: ResourceState, + _unused: u8, } #[jsg_resource] @@ -158,11 +157,8 @@ impl ArrayResource { fn resource_accepts_array_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let result: Number = ctx.eval(lock, "arr.sum([1, 2, 3, 4, 5])").unwrap(); @@ -180,11 +176,8 @@ fn resource_accepts_array_parameter() { fn resource_accepts_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let result: Number = ctx.eval(lock, "arr.sumSlice([1, 2, 3, 4, 5])").unwrap(); @@ -203,11 +196,8 @@ fn resource_accepts_slice_parameter() { fn resource_returns_array() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let result: Vec = ctx.eval(lock, "arr.double([1, 2, 3])").unwrap(); @@ -230,11 +220,8 @@ fn resource_returns_array() { fn resource_accepts_typed_array_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let result: Vec = ctx @@ -254,11 +241,8 @@ fn resource_accepts_typed_array_parameter() { fn resource_returns_typed_array() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let is_u8: bool = ctx @@ -276,11 +260,8 @@ fn resource_returns_typed_array() { fn resource_accepts_typed_array_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test &[u8] accepts Uint8Array @@ -309,11 +290,8 @@ fn resource_accepts_typed_array_slice_parameter() { fn typed_array_slice_rejects_wrong_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // &[u8] should reject Int8Array @@ -342,11 +320,8 @@ fn typed_array_slice_rejects_wrong_type() { fn resource_accepts_and_returns_struct_array() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); let result: Vec = ctx @@ -695,11 +670,8 @@ fn typed_array_empty() { fn float32_array_parameter_and_return() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test Vec parameter @@ -730,11 +702,8 @@ fn float32_array_parameter_and_return() { fn float32_array_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test &[f32] parameter @@ -757,11 +726,8 @@ fn float32_array_slice_parameter() { fn float32_array_rejects_wrong_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Float32Array should reject Float64Array @@ -825,11 +791,8 @@ fn float32_array_to_js_and_from_js() { fn float64_array_parameter_and_return() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test Vec parameter @@ -860,11 +823,8 @@ fn float64_array_parameter_and_return() { fn float64_array_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test &[f64] parameter @@ -887,11 +847,8 @@ fn float64_array_slice_parameter() { fn float64_array_rejects_wrong_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Float64Array should reject Float32Array @@ -942,11 +899,8 @@ fn float64_array_to_js_and_from_js() { fn bigint64_array_parameter_and_return() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test Vec parameter @@ -983,11 +937,8 @@ fn bigint64_array_parameter_and_return() { fn bigint64_array_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test &[i64] parameter @@ -1010,11 +961,8 @@ fn bigint64_array_slice_parameter() { fn bigint64_array_rejects_wrong_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // BigInt64Array should reject BigUint64Array @@ -1064,11 +1012,8 @@ fn bigint64_array_to_js_and_from_js() { fn biguint64_array_parameter_and_return() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test Vec parameter @@ -1099,11 +1044,8 @@ fn biguint64_array_parameter_and_return() { fn biguint64_array_slice_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // Test &[u64] parameter @@ -1126,11 +1068,8 @@ fn biguint64_array_slice_parameter() { fn biguint64_array_rejects_wrong_type() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(ArrayResource { - _state: ResourceState::default(), - }); - let mut template = ArrayResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = ArrayResource::alloc(lock, ArrayResource { _unused: 0 }); + let wrapped = ArrayResource::wrap(resource, lock); ctx.set_global("arr", wrapped); // BigUint64Array should reject BigInt64Array diff --git a/src/rust/jsg-test/tests/gc.rs b/src/rust/jsg-test/tests/gc.rs new file mode 100644 index 00000000000..83f41b69e11 --- /dev/null +++ b/src/rust/jsg-test/tests/gc.rs @@ -0,0 +1,392 @@ +//! GC (garbage collection) tests for Rust resources. +//! +//! These tests verify that Rust resources are properly cleaned up when: +//! 1. All Rust `Ref` handles are dropped and no JavaScript wrapper exists +//! 2. All Rust `Ref` handles are dropped and V8 garbage collects the JS wrapper + +use std::cell::RefCell; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use jsg::Resource; +use jsg::v8::TracedReference; +use jsg_macros::jsg_method; +use jsg_macros::jsg_resource; + +/// Counter to track how many `SimpleResource` instances have been dropped. +static SIMPLE_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0); + +#[jsg_resource] +struct SimpleResource { + pub name: String, + pub callback: Option>, +} + +impl Drop for SimpleResource { + fn drop(&mut self) { + SIMPLE_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst); + } +} + +#[jsg_resource] +#[expect(clippy::unnecessary_wraps)] +impl SimpleResource { + #[jsg_method] + fn get_name(&self) -> Result { + Ok(self.name.clone()) + } +} + +/// Counter to track how many `ParentResource` instances have been dropped. +static PARENT_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0); + +#[jsg_resource] +struct ParentResource { + pub child: jsg::Ref, + pub optional_child: Option>, +} + +impl Drop for ParentResource { + fn drop(&mut self) { + PARENT_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst); + } +} + +#[jsg_resource] +impl ParentResource {} + +/// Counter to track how many `CyclicResource` instances have been dropped. +static CYCLIC_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0); + +#[jsg_resource] +struct CyclicResource { + #[expect(dead_code)] + pub name: String, + pub other: RefCell>>, +} + +impl Drop for CyclicResource { + fn drop(&mut self) { + CYCLIC_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst); + } +} + +#[jsg_resource] +impl CyclicResource {} + +/// Tests that resources are dropped when all Rust Refs are dropped and no JS wrapper exists. +/// +/// When a resource is allocated but never wrapped for JavaScript, dropping all `Ref` handles +/// should immediately deallocate the resource. +#[test] +fn supports_gc_via_ref_drop() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let resource = SimpleResource::alloc( + lock, + SimpleResource { + name: "test".to_owned(), + callback: None, + }, + ); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + std::mem::drop(resource); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +/// Tests that resources are dropped via V8 GC weak callback when JS wrapper is collected. +/// +/// When a resource is wrapped for JavaScript: +/// 1. Dropping all Rust `Ref` handles makes the V8 Global weak +/// 2. V8 GC can then collect the wrapper and trigger the weak callback +/// 3. The weak callback deallocates the resource +#[test] +fn supports_gc_via_weak_callback() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let resource = SimpleResource::alloc( + lock, + SimpleResource { + name: "test".to_owned(), + callback: None, + }, + ); + let _wrapped = SimpleResource::wrap(resource.clone(), lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + std::mem::drop(resource); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn resource_with_traced_ref_field() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let child = SimpleResource::alloc( + lock, + SimpleResource { + name: "child".to_owned(), + callback: None, + }, + ); + let optional_child = SimpleResource::alloc( + lock, + SimpleResource { + name: "optional_child".to_owned(), + callback: None, + }, + ); + + let parent = ParentResource::alloc( + lock, + ParentResource { + child: child.clone(), + optional_child: Some(optional_child.clone()), + }, + ); + + let _wrapped = ParentResource::wrap(parent.clone(), lock); + + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + + std::mem::drop(child); + std::mem::drop(optional_child); + std::mem::drop(parent); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + crate::Harness::request_gc(lock); + assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 2); + Ok(()) + }); +} + +#[test] +fn child_traced_ref_kept_alive_by_parent() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let child = SimpleResource::alloc( + lock, + SimpleResource { + name: "child".to_owned(), + callback: None, + }, + ); + + let parent = ParentResource::alloc( + lock, + ParentResource { + child: child.clone(), + optional_child: None, + }, + ); + + let _parent_wrapped = ParentResource::wrap(parent.clone(), lock); + + // Child not collected because parent still holds a reference + std::mem::drop(child); + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + + std::mem::drop(parent); + crate::Harness::request_gc(lock); + assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn weak_ref_upgrade() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let strong = SimpleResource::alloc( + lock, + SimpleResource { + name: "test".to_owned(), + callback: None, + }, + ); + let weak = jsg::WeakRef::from(&strong); + + assert!(weak.is_alive()); + let upgraded = weak.upgrade(); + assert!(upgraded.is_some()); + assert!(weak.is_alive()); + + std::mem::drop(upgraded); + assert!(weak.is_alive()); + + std::mem::drop(strong); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn cppgc_handle_keeps_resource_alive() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let strong = SimpleResource::alloc( + lock, + SimpleResource { + name: "test".to_owned(), + callback: None, + }, + ); + let _wrapped = SimpleResource::wrap(strong.clone(), lock); + std::mem::drop(strong); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn cppgc_weak_member_cleared_after_gc() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let strong = SimpleResource::alloc( + lock, + SimpleResource { + name: "test".to_owned(), + callback: None, + }, + ); + let _wrapped = SimpleResource::wrap(strong.clone(), lock); + let weak = jsg::WeakRef::from(&strong); + + assert!(weak.upgrade().is_some()); + std::mem::drop(strong); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn cppgc_traced_ref_in_gc() { + SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst); + PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let child = SimpleResource::alloc( + lock, + SimpleResource { + name: "child".to_owned(), + callback: None, + }, + ); + let _child_wrapped = SimpleResource::wrap(child.clone(), lock); + + let parent = ParentResource::alloc( + lock, + ParentResource { + child: child.clone(), + optional_child: None, + }, + ); + let _parent_wrapped = ParentResource::wrap(parent.clone(), lock); + + std::mem::drop(child); + std::mem::drop(parent); + + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + crate::Harness::request_gc(lock); + assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + crate::Harness::request_gc(lock); + assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1); + Ok(()) + }); +} + +#[test] +fn circular_references_can_be_collected() { + CYCLIC_RESOURCE_DROPS.store(0, Ordering::SeqCst); + + let harness = crate::Harness::new(); + harness.run_in_context(|lock, _ctx| { + let ref_a = CyclicResource::alloc( + lock, + CyclicResource { + name: "A".to_owned(), + other: RefCell::new(None), + }, + ); + let ref_b = CyclicResource::alloc( + lock, + CyclicResource { + name: "B".to_owned(), + other: RefCell::new(None), + }, + ); + + // Create circular reference: A -> B -> A + *ref_a.other.borrow_mut() = Some(ref_b.clone()); + *ref_b.other.borrow_mut() = Some(ref_a.clone()); + + let _wrapped_a = CyclicResource::wrap(ref_a.clone(), lock); + let _wrapped_b = CyclicResource::wrap(ref_b.clone(), lock); + + std::mem::drop(ref_a); + std::mem::drop(ref_b); + assert_eq!(CYCLIC_RESOURCE_DROPS.load(Ordering::SeqCst), 0); + Ok(()) + }); + + harness.run_in_context(|lock, _ctx| { + crate::Harness::request_gc(lock); + crate::Harness::request_gc(lock); + assert_eq!(CYCLIC_RESOURCE_DROPS.load(Ordering::SeqCst), 2); + Ok(()) + }); +} diff --git a/src/rust/jsg-test/tests/jsg_oneof.rs b/src/rust/jsg-test/tests/jsg_oneof.rs index bd4b7b746a4..f2cfc4600a1 100644 --- a/src/rust/jsg-test/tests/jsg_oneof.rs +++ b/src/rust/jsg-test/tests/jsg_oneof.rs @@ -1,7 +1,6 @@ use jsg::ExceptionType; use jsg::Number; -use jsg::ResourceState; -use jsg::ResourceTemplate; +use jsg::Resource; use jsg_macros::jsg_method; use jsg_macros::jsg_oneof; use jsg_macros::jsg_resource; @@ -37,7 +36,7 @@ enum ThreeTypes { #[jsg_resource] struct EnumTestResource { - _state: ResourceState, + _unused: u8, } #[jsg_resource] @@ -100,11 +99,8 @@ fn jsg_oneof_derives_debug_and_clone() { fn jsg_oneof_string_or_number_accepts_string() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: String = ctx.eval(lock, "resource.stringOrNumber('hello')").unwrap(); @@ -117,11 +113,8 @@ fn jsg_oneof_string_or_number_accepts_string() { fn jsg_oneof_string_or_number_accepts_number() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: String = ctx.eval(lock, "resource.stringOrNumber(42)").unwrap(); @@ -134,11 +127,8 @@ fn jsg_oneof_string_or_number_accepts_number() { fn jsg_oneof_string_or_number_rejects_boolean() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let err = ctx @@ -156,11 +146,8 @@ fn jsg_oneof_string_or_number_rejects_boolean() { fn jsg_oneof_string_or_bool_accepts_both() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: String = ctx.eval(lock, "resource.stringOrBool('test')").unwrap(); @@ -176,11 +163,8 @@ fn jsg_oneof_string_or_bool_accepts_both() { fn jsg_oneof_three_types_accepts_all() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: String = ctx.eval(lock, "resource.threeTypes('hello')").unwrap(); @@ -199,11 +183,8 @@ fn jsg_oneof_three_types_accepts_all() { fn jsg_oneof_three_types_rejects_null_and_undefined() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let err = ctx @@ -225,11 +206,8 @@ fn jsg_oneof_three_types_rejects_null_and_undefined() { fn jsg_oneof_variant_order_matches_declaration() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); // StringOrNumber has String first, Number second @@ -253,11 +231,8 @@ fn jsg_oneof_variant_order_matches_declaration() { fn jsg_oneof_reference_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EnumTestResource { - _state: ResourceState::default(), - }); - let mut template = EnumTestResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EnumTestResource::alloc(lock, EnumTestResource { _unused: 0 }); + let wrapped = EnumTestResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: String = ctx diff --git a/src/rust/jsg-test/tests/mod.rs b/src/rust/jsg-test/tests/mod.rs index 32f3a1ef01b..abaacde8879 100644 --- a/src/rust/jsg-test/tests/mod.rs +++ b/src/rust/jsg-test/tests/mod.rs @@ -1,5 +1,6 @@ mod arrays; mod eval; +mod gc; mod jsg_oneof; mod jsg_struct; mod non_coercible; diff --git a/src/rust/jsg-test/tests/non_coercible.rs b/src/rust/jsg-test/tests/non_coercible.rs index 9d9c1fb41fe..1d9ef3a262c 100644 --- a/src/rust/jsg-test/tests/non_coercible.rs +++ b/src/rust/jsg-test/tests/non_coercible.rs @@ -1,14 +1,13 @@ use jsg::ExceptionType; use jsg::NonCoercible; use jsg::Number; -use jsg::ResourceState; -use jsg::ResourceTemplate; +use jsg::Resource; use jsg_macros::jsg_method; use jsg_macros::jsg_resource; #[jsg_resource] struct MyResource { - _state: ResourceState, + _unused: u32, } #[jsg_resource] @@ -126,11 +125,8 @@ fn non_coercible_debug() { fn non_coercible_methods_accept_correct_types_and_reject_incorrect_types() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(MyResource { - _state: ResourceState::default(), - }); - let mut template = MyResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = MyResource::alloc(lock, MyResource { _unused: 0 }); + let wrapped = MyResource::wrap(resource, lock); ctx.set_global("resource", wrapped); // String method accepts string diff --git a/src/rust/jsg-test/tests/resource_callback.rs b/src/rust/jsg-test/tests/resource_callback.rs index 3d547c05989..e6b600a63d3 100644 --- a/src/rust/jsg-test/tests/resource_callback.rs +++ b/src/rust/jsg-test/tests/resource_callback.rs @@ -9,14 +9,12 @@ use std::cell::Cell; use std::rc::Rc; use jsg::Number; -use jsg::ResourceState; -use jsg::ResourceTemplate; +use jsg::Resource; use jsg_macros::jsg_method; use jsg_macros::jsg_resource; #[jsg_resource] struct EchoResource { - _state: ResourceState, prefix: String, } @@ -36,7 +34,6 @@ impl EchoResource { #[jsg_resource] struct DirectReturnResource { - _state: ResourceState, name: String, counter: Rc>, } @@ -76,12 +73,13 @@ impl DirectReturnResource { fn resource_method_callback_receives_correct_self() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EchoResource { - _state: ResourceState::default(), - prefix: "Hello, ".to_owned(), - }); - let mut template = EchoResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EchoResource::alloc( + lock, + EchoResource { + prefix: "Hello, ".to_owned(), + }, + ); + let wrapped = EchoResource::wrap(resource, lock); ctx.set_global("echoResource", wrapped); // Call the method from JavaScript @@ -96,12 +94,13 @@ fn resource_method_callback_receives_correct_self() { fn resource_method_can_be_called_multiple_times() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EchoResource { - _state: ResourceState::default(), - prefix: ">> ".to_owned(), - }); - let mut template = EchoResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EchoResource::alloc( + lock, + EchoResource { + prefix: ">> ".to_owned(), + }, + ); + let wrapped = EchoResource::wrap(resource, lock); ctx.set_global("echo", wrapped); // First call @@ -120,12 +119,13 @@ fn resource_method_can_be_called_multiple_times() { fn resource_method_accepts_str_ref_parameter() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(EchoResource { - _state: ResourceState::default(), - prefix: "Hello, ".to_owned(), - }); - let mut template = EchoResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = EchoResource::alloc( + lock, + EchoResource { + prefix: "Hello, ".to_owned(), + }, + ); + let wrapped = EchoResource::wrap(resource, lock); ctx.set_global("echo", wrapped); let result: String = ctx.eval(lock, "echo.greet('World')").unwrap(); @@ -140,13 +140,14 @@ fn resource_method_returns_non_result_values() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { let counter = Rc::new(Cell::new(42)); - let resource = jsg::Ref::new(DirectReturnResource { - _state: ResourceState::default(), - name: "TestResource".to_owned(), - counter: counter.clone(), - }); - let mut template = DirectReturnResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = DirectReturnResource::alloc( + lock, + DirectReturnResource { + name: "TestResource".to_owned(), + counter: counter.clone(), + }, + ); + let wrapped = DirectReturnResource::wrap(resource, lock); ctx.set_global("resource", wrapped); // Test getString returns string @@ -177,13 +178,14 @@ fn resource_method_returns_non_result_values() { fn resource_method_returns_null_for_none() { let harness = crate::Harness::new(); harness.run_in_context(|lock, ctx| { - let resource = jsg::Ref::new(DirectReturnResource { - _state: ResourceState::default(), - name: String::new(), - counter: Rc::new(Cell::new(0)), - }); - let mut template = DirectReturnResourceTemplate::new(lock); - let wrapped = unsafe { jsg::wrap_resource(lock, resource, &mut template) }; + let resource = DirectReturnResource::alloc( + lock, + DirectReturnResource { + name: String::new(), + counter: Rc::new(Cell::new(0)), + }, + ); + let wrapped = DirectReturnResource::wrap(resource, lock); ctx.set_global("resource", wrapped); let result: Option = ctx.eval(lock, "resource.maybeName()").unwrap(); diff --git a/src/rust/jsg/README.md b/src/rust/jsg/README.md index 0b67a13e8b5..e37a442b53a 100644 --- a/src/rust/jsg/README.md +++ b/src/rust/jsg/README.md @@ -2,6 +2,231 @@ Rust bindings for the JSG (JavaScript Glue) layer, enabling Rust code to integrate with workerd's JavaScript runtime. +## Core Types + +### `Isolate` + +A safe wrapper around V8's `v8::Isolate*` pointer. This type ensures the pointer is always non-null and provides methods to interact with the V8 isolate. + +```rust +// Creating from a raw pointer (unsafe - pointer must be valid and non-null) +let isolate = unsafe { v8::Isolate::from_raw(raw_ptr) }; + +// Getting the raw pointer for FFI calls +let ptr = isolate.as_ptr(); + +// Checking if the isolate is locked by the current thread +if isolate.is_locked() { + // Safe to perform V8 operations +} +``` + +### `Lock` + +Provides access to V8 operations within an isolate lock. Passed to resource methods and callbacks to perform V8 operations. + +```rust +impl Lock { + // Create from an Isolate + pub fn from_isolate(isolate: v8::Isolate) -> Self; + + // Get the underlying isolate + pub fn isolate(&self) -> v8::Isolate; + + // Create a new JavaScript object + pub fn new_object<'a>(&mut self) -> v8::Local<'a, v8::Object>; +} +``` + +### `Realm` + +Per-isolate state for Rust resources exposed to JavaScript. Stores cached function templates and tracks resource instances. + +## Garbage Collection with cppgc (Oilpan) + +Resources exposed to JavaScript are managed by V8's cppgc (Oilpan) garbage collector with full support for cycle collection. + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ cppgc Heap │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ RustResource (C++ GarbageCollected object) │ │ +│ │ ├─ data[2]: fat pointer to dyn GarbageCollected │ │ +│ │ └─ [AdditionalBytes]: Instance │ │ +│ │ ├─ resource: R (user's Rust struct) │ │ +│ │ ├─ wrapper: Option> │ │ +│ │ └─ strong_refcount: Cell │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ▲ ▲ + │ │ + ┌────┴────┐ ┌─────┴─────┐ + │ Handle │ │ Member │ + │(Persist)│ │ (traced) │ + └────┬────┘ └─────┬─────┘ + │ │ + └──────────┬───────────────────┘ + │ + ┌─────┴─────┐ + │ Ref │ ← dynamically switches between Handle/Member + │ (storage) │ + └───────────┘ +``` + +### RustResource: The Bridge Between C++ and Rust + +cppgc can only trace C++ classes that inherit from `cppgc::GarbageCollected`. `RustResource` acts as a bridge: cppgc sees a normal C++ GC object, but the actual data is a Rust object stored in cppgc's `AdditionalBytes` region. + +```cpp +// C++ side (ffi.h) +class RustResource: public cppgc::GarbageCollected { +public: + ~RustResource(); // Calls Rust drop + void Trace(cppgc::Visitor*) const; // Calls Rust trace + uintptr_t data[2]; // Fat pointer: [data_ptr, vtable_ptr] +}; +``` + +The `data[2]` field stores a Rust fat pointer (`dyn GarbageCollected`) enabling: +- **Tracing**: When cppgc traces the object, it invokes `GarbageCollected::trace()` through the vtable +- **Destruction**: When cppgc collects the object, the destructor calls Rust's `drop()` + +### Instance: The Rust Wrapper + +Each resource is wrapped in `Instance` which lives in the `AdditionalBytes` region: + +```rust +struct Instance { + resource: R, // User's data + wrapper: Option>, // JS wrapper (if wrapped) + strong_refcount: Cell, // Active Ref count +} +``` + +### How Allocation Works + +```rust +let resource = MyResource::alloc(&mut lock, MyResource { ... }); +``` + +1. `cppgc::MakeGarbageCollected` allocates a `RustResource` with extra bytes for `Instance` +2. The Rust object is written into the `AdditionalBytes` region +3. A fat pointer to `dyn GarbageCollected` is stored in `RustResource::data[2]` +4. A `Ref` with a `Handle` (strong mode) is returned + +### How GC Tracing Works + +When cppgc runs a GC cycle: + +1. **C++ Trace**: `RustResource::Trace()` is called by cppgc +2. **FFI Bridge**: Calls `cppgc_invoke_trace()` which reads the fat pointer from `data[2]` +3. **Rust Trace**: Invokes `GarbageCollected::trace()` on the Rust object +4. **Visit Children**: The trace method visits all `Ref`, `TracedReference` fields + +### Dynamic Mode Switching in Ref + +`Ref` uses `RefStorage` to hold either a `Handle` (strong) or `Member` (traced): + +```rust +enum RefStorage { + Strong(Handle), // cppgc::Persistent - prevents collection + Traced(Member), // cppgc::Member - traced, enables cycles +} +``` + +The switch happens during GC visitation based on the parent's state: +- **Switch to Traced**: When parent has a JS wrapper (reachable from JS) +- **Switch to Strong**: When parent loses JS wrapper but still has Rust refs + +This enables cycle collection: when circular `Ref` references use `Member` internally, cppgc can detect and collect the entire cycle when unreachable. + +### How it works + +1. **Allocation**: Resources are allocated on the cppgc heap via `Resource::alloc()`, returning a `Ref` handle. + +2. **Dynamic mode switching**: `Ref` dynamically switches between two modes: + - **Strong mode** (`cppgc::Persistent`): Used when held by Rust code outside the GC heap. Prevents collection. + - **Traced mode** (`cppgc::Member`): Used when owned by a parent resource that has a JavaScript wrapper. Enables cycle collection. + +3. **Wrapping**: When a resource is wrapped for JavaScript via `wrap()`, the wrapper is stored as a `TracedReference`. Child `Ref` fields automatically switch to traced mode during GC visitation. + +4. **Cycle collection**: Because traced `Ref` fields use `cppgc::Member` internally, circular references between resources are properly collected when no JavaScript references remain. + +5. **Tracing**: The `#[jsg_resource]` macro generates `GarbageCollected::trace()` implementations that visit all `Ref`, `TracedReference`, and `RefCell>>` fields. + +### Resource Reference Types + +| Type | Description | +|------|-------------| +| `Ref` | Smart pointer to a resource. Dynamically switches between strong and traced modes. Derefs to `&R`. | +| `WeakRef` | Weak reference that doesn't prevent collection. Use `upgrade()` to get `Option>`. | + +### cppgc Types + +The `v8::cppgc` module provides Rust wrappers for cppgc's reference types: + +| Type | Description | Needs Tracing | +|------|-------------|---------------| +| `Handle` | Strong off-heap → on-heap reference (`cppgc::Persistent`) | No | +| `WeakHandle` | Weak off-heap → on-heap reference (`cppgc::WeakPersistent`) | No | +| `Member` | Strong on-heap → on-heap reference (`cppgc::Member`) | Yes | +| `WeakMember` | Weak on-heap → on-heap reference (`cppgc::WeakMember`) | Yes | + +### Example + +```rust +#[jsg_resource] +pub struct ParentResource { + name: String, + // Child resources - automatically traced + child: Ref, + optional_child: Option>, + // JavaScript callbacks - automatically traced + callback: Option>, +} + +// Cyclic references using RefCell +#[jsg_resource] +pub struct Node { + name: String, + next: RefCell>>, +} + +// Usage: create a cycle that will be properly collected +let node_a = Node::alloc(&mut lock, Node { name: "A".into(), next: RefCell::new(None) }); +let node_b = Node::alloc(&mut lock, Node { name: "B".into(), next: RefCell::new(None) }); +*node_a.next.borrow_mut() = Some(node_b.clone()); +*node_b.next.borrow_mut() = Some(node_a.clone()); +// When wrapped and later unreferenced from JS, both nodes will be collected +``` + +## V8 Handle Types + +### `Local<'a, T>` + +A stack-allocated handle to a V8 value. The lifetime `'a` is tied to the `HandleScope` that created it. + +```rust +// Creating locals +let str_value = "hello".to_local(&mut lock); +let num_value = 42u32.to_local(&mut lock); + +// Converting to global +let global = local.to_global(&mut lock); +``` + +### `Global` + +A persistent handle that outlives `HandleScope`s. Must be explicitly managed. + +### `TracedReference` + +A reference traced by the garbage collector. Used for storing V8 handles in cppgc-managed objects. + +**Note**: `TracedReference` intentionally does NOT implement `Drop`. V8 manages the lifecycle of traced handles automatically during GC. Calling `reset()` during cppgc finalization would cause use-after-free. + ## FFI Functions with Raw Pointers Functions exposed to C++ via FFI that receive raw pointers must be marked as `unsafe fn`. The `unsafe` keyword indicates to callers that the function deals with raw pointers and requires careful handling. diff --git a/src/rust/jsg/ffi-inl.h b/src/rust/jsg/ffi-inl.h index 583a4c3ae95..e505a2c76c4 100644 --- a/src/rust/jsg/ffi-inl.h +++ b/src/rust/jsg/ffi-inl.h @@ -62,4 +62,49 @@ inline v8::Global* global_as_ref_from_ffi(Global& value) { return reinterpret_cast*>(ptr_void); } +// TracedReference +static_assert( + sizeof(v8::TracedReference) == sizeof(TracedReference), "Size should match"); +static_assert( + alignof(v8::TracedReference) == alignof(TracedReference), "Alignment should match"); + +template +inline TracedReference to_ffi(v8::TracedReference&& value) { + size_t result; + auto ptr_void = reinterpret_cast(&result); + new (ptr_void) v8::TracedReference(kj::mv(value)); + return TracedReference{result}; +} + +template +inline v8::TracedReference traced_reference_from_ffi(TracedReference&& value) { + auto ptr_void = reinterpret_cast(&value.ptr); + return kj::mv(*reinterpret_cast*>(ptr_void)); +} + +template +inline const v8::TracedReference& traced_reference_as_ref_from_ffi( + const TracedReference& value) { + auto ptr_void = reinterpret_cast(&value.ptr); + return *reinterpret_cast*>(ptr_void); +} + +template +inline v8::TracedReference* traced_reference_as_ref_from_ffi(TracedReference& value) { + auto ptr_void = reinterpret_cast(&value.ptr); + return reinterpret_cast*>(ptr_void); +} + +// CppgcVisitor - wraps a pointer to cppgc::Visitor +static_assert(sizeof(cppgc::Visitor*) == sizeof(CppgcVisitor), "Size should match"); +static_assert(alignof(cppgc::Visitor*) == alignof(CppgcVisitor), "Alignment should match"); + +inline CppgcVisitor to_ffi(cppgc::Visitor* visitor) { + return CppgcVisitor{reinterpret_cast(visitor)}; +} + +inline cppgc::Visitor* cppgc_visitor_from_ffi(CppgcVisitor* value) { + return reinterpret_cast(value->ptr); +} + } // namespace workerd::rust::jsg diff --git a/src/rust/jsg/ffi.c++ b/src/rust/jsg/ffi.c++ index 69086a8476a..20783c11a1e 100644 --- a/src/rust/jsg/ffi.c++ +++ b/src/rust/jsg/ffi.c++ @@ -4,10 +4,13 @@ #include #include #include +#include #include #include +#include + using namespace kj_rs; namespace workerd::rust::jsg { @@ -46,6 +49,20 @@ namespace workerd::rust::jsg { // ============================================================================= +// RustResource implementation - calls into Rust via CXX bridge +RustResource::~RustResource() { + cppgc_invoke_drop(this); +} + +void RustResource::Trace(cppgc::Visitor* visitor) const { + auto ffi_visitor = to_ffi(visitor); + cppgc_invoke_trace(this, &ffi_visitor); +} + +const char* RustResource::GetHumanReadableName() const { + return cppgc_invoke_get_name(this); +} + // Local void local_drop(Local value) { // Convert from FFI representation and let v8::Local destructor handle cleanup @@ -254,7 +271,7 @@ DEFINE_TYPED_ARRAY_NEW(bigint64_array, BigInt64Array, int64_t) DEFINE_TYPED_ARRAY_NEW(biguint64_array, BigUint64Array, uint64_t) // Wrappers -Local wrap_resource(Isolate* isolate, size_t resource, const Global& tmpl, size_t drop_callback) { +Local wrap_resource(Isolate* isolate, size_t resource, const Global& tmpl) { auto self = reinterpret_cast(resource); auto& global_tmpl = global_as_ref_from_ffi(tmpl); auto local_tmpl = v8::Local::New(isolate, global_tmpl); @@ -364,8 +381,9 @@ DEFINE_TYPED_ARRAY_GET(bigint64_array, BigInt64Array, int64_t) DEFINE_TYPED_ARRAY_GET(biguint64_array, BigUint64Array, uint64_t) // Global -void global_drop(Global value) { - global_from_ffi(kj::mv(value)); +void global_reset(Global* value) { + auto* glbl = global_as_ref_from_ffi(*value); + glbl->Reset(); } Global global_clone(const Global& value) { @@ -378,13 +396,25 @@ Local global_to_local(Isolate* isolate, const Global& value) { return to_ffi(kj::mv(local)); } -void global_make_weak(Isolate* isolate, Global* value, size_t data, WeakCallback callback) { - // callback is unused; GC-based cleanup not yet implemented. - // Cleanup happens in Realm::drop() during context disposal. - auto glbl = global_as_ref_from_ffi(*value); - glbl->SetWeak(reinterpret_cast(data), [](const v8::WeakCallbackInfo& info) { - KJ_UNIMPLEMENTED("global_make_weak"); - }, v8::WeakCallbackType::kParameter); +// TracedReference +TracedReference traced_reference_from_local(Isolate* isolate, Local value) { + v8::TracedReference traced(isolate, local_from_ffi(kj::mv(value))); + return to_ffi(kj::mv(traced)); +} + +Local traced_reference_to_local(Isolate* isolate, const TracedReference& value) { + auto& traced = traced_reference_as_ref_from_ffi(value); + return to_ffi(traced.Get(isolate)); +} + +void traced_reference_reset(TracedReference* value) { + auto* traced = traced_reference_as_ref_from_ffi(*value); + traced->Reset(); +} + +bool traced_reference_is_empty(const TracedReference& value) { + auto& traced = traced_reference_as_ref_from_ffi(value); + return traced.IsEmpty(); } // FunctionCallbackInfo @@ -527,4 +557,139 @@ bool isolate_is_locked(Isolate* isolate) { return v8::Locker::IsLocked(isolate); } +// cppgc - Allocate Rust objects directly on the GC heap +size_t cppgc_rust_resource_size() { + return sizeof(RustResource); +} + +RustResource* cppgc_make_garbage_collected(Isolate* isolate, size_t size, size_t alignment) { + auto* heap = isolate->GetCppHeap(); + KJ_ASSERT(heap != nullptr, "CppHeap not available on isolate"); + KJ_ASSERT(alignment <= 16, "Alignment {} exceeds maximum of 16", alignment); + + // Allocate RustResource with additional bytes for the Rust object. + // The Rust object will be written into the space after the RustResource header. + if (alignment <= 8) { + return cppgc::MakeGarbageCollected( + heap->GetAllocationHandle(), cppgc::AdditionalBytes(size)); + } + + return cppgc::MakeGarbageCollected( + heap->GetAllocationHandle(), cppgc::AdditionalBytes(size)); +} + +uintptr_t* cppgc_rust_resource_data(RustResource* resource) { + return resource->data; +} + +const uintptr_t* cppgc_rust_resource_data_const(const RustResource* resource) { + return resource->data; +} + +void cppgc_visitor_trace(CppgcVisitor* visitor, const TracedReference& handle) { + auto* v8_visitor = cppgc_visitor_from_ffi(visitor); + auto& traced = traced_reference_as_ref_from_ffi(handle); + v8_visitor->Trace(traced); +} + +// Persistent inline storage functions +// Note: cppgc::Persistent stores an internal pointer to a PersistentNode, so it can be +// stored inline without issues. The internal node is heap-allocated by cppgc. + +size_t cppgc_persistent_size() { + return sizeof(CppgcPersistent); +} + +void cppgc_persistent_construct(size_t storage, RustResource* resource) { + new (reinterpret_cast(storage)) CppgcPersistent(resource); +} + +void cppgc_persistent_destruct(size_t storage) { + std::destroy_at(reinterpret_cast(storage)); +} + +RustResource* cppgc_persistent_get(size_t storage) { + return reinterpret_cast(storage)->Get(); +} + +void cppgc_persistent_assign(size_t storage, RustResource* resource) { + *reinterpret_cast(storage) = resource; +} + +// WeakPersistent inline storage functions + +size_t cppgc_weak_persistent_size() { + return sizeof(CppgcWeakPersistent); +} + +void cppgc_weak_persistent_construct(size_t storage, RustResource* resource) { + new (reinterpret_cast(storage)) CppgcWeakPersistent(resource); +} + +void cppgc_weak_persistent_destruct(size_t storage) { + std::destroy_at(reinterpret_cast(storage)); +} + +RustResource* cppgc_weak_persistent_get(size_t storage) { + return reinterpret_cast(storage)->Get(); +} + +void cppgc_weak_persistent_assign(size_t storage, RustResource* resource) { + *reinterpret_cast(storage) = resource; +} + +// Member inline storage functions + +size_t cppgc_member_size() { + return sizeof(CppgcMember); +} + +void cppgc_member_construct(size_t storage, RustResource* resource) { + new (reinterpret_cast(storage)) CppgcMember(resource); +} + +void cppgc_member_destruct(size_t storage) { + std::destroy_at(reinterpret_cast(storage)); +} + +RustResource* cppgc_member_get(size_t storage) { + return reinterpret_cast(storage)->Get(); +} + +void cppgc_member_assign(size_t storage, RustResource* resource) { + *reinterpret_cast(storage) = resource; +} + +void cppgc_visitor_trace_member(CppgcVisitor* visitor, size_t storage) { + auto* v8_visitor = cppgc_visitor_from_ffi(visitor); + v8_visitor->Trace(*reinterpret_cast(storage)); +} + +// WeakMember inline storage functions + +size_t cppgc_weak_member_size() { + return sizeof(CppgcWeakMember); +} + +void cppgc_weak_member_construct(size_t storage, RustResource* resource) { + new (reinterpret_cast(storage)) CppgcWeakMember(resource); +} + +void cppgc_weak_member_destruct(size_t storage) { + std::destroy_at(reinterpret_cast(storage)); +} + +RustResource* cppgc_weak_member_get(size_t storage) { + return reinterpret_cast(storage)->Get(); +} + +void cppgc_weak_member_assign(size_t storage, RustResource* resource) { + *reinterpret_cast(storage) = resource; +} + +void cppgc_visitor_trace_weak_member(CppgcVisitor* visitor, size_t storage) { + auto* v8_visitor = cppgc_visitor_from_ffi(visitor); + v8_visitor->Trace(*reinterpret_cast(storage)); +} + } // namespace workerd::rust::jsg diff --git a/src/rust/jsg/ffi.h b/src/rust/jsg/ffi.h index 387b0eff37a..b9a5723968f 100644 --- a/src/rust/jsg/ffi.h +++ b/src/rust/jsg/ffi.h @@ -2,8 +2,15 @@ #include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include @@ -12,17 +19,46 @@ // Forward declarations needed by v8.rs.h namespace workerd::rust::jsg { using Isolate = v8::Isolate; -using Context = v8::Local; using FunctionCallbackInfo = v8::FunctionCallbackInfo; -using WeakCallbackInfo = v8::WeakCallbackInfo; struct ModuleRegistry; struct Local; struct Global; struct Realm; +struct TracedReference; +struct CppgcVisitor; enum class ExceptionType : ::std::uint8_t; -using ModuleType = ::workerd::jsg::ModuleType; +// ModuleType enum is generated by CXX in v8.rs.h - forward declare it here +enum class ModuleType : ::std::uint8_t; using ModuleCallback = ::rust::Fn; -using WeakCallback = ::rust::Fn; + +// A cppgc-managed wrapper that allows Rust objects to live on V8's garbage-collected heap. +// +// We need this because cppgc can only trace C++ classes that inherit from GarbageCollected. +// RustResource acts as a bridge: cppgc sees a normal C++ GC object, but the actual data +// is a Rust object stored in cppgc's AdditionalBytes region right after this header. +// +// The data[] field stores a Rust fat pointer (data + vtable) to `dyn GarbageCollected`. +// This lets us call back into Rust for tracing and destruction without knowing the +// concrete type at compile time. When cppgc traces this object, we invoke the Rust +// trace method through the vtable. When cppgc collects it, the destructor calls Rust's drop. +class RustResource: public cppgc::GarbageCollected, public cppgc::NameProvider { + public: + ~RustResource(); + void Trace(cppgc::Visitor* visitor) const; + const char* GetHumanReadableName() const final; + + uintptr_t data[2]; // Rust fat pointer: [data_ptr, vtable_ptr] +}; + +// Used when allocating Rust objects that require 16-byte alignment. +// cppgc's maximum supported alignment is 16 bytes. When the Rust type's alignment +// exceeds 8 bytes, we use this class to ensure the cppgc allocation is 16-byte aligned. +class alignas(16) RustResourceAlign16: public RustResource {}; + +using CppgcPersistent = cppgc::Persistent; +using CppgcWeakPersistent = cppgc::WeakPersistent; +using CppgcMember = cppgc::Member; +using CppgcWeakMember = cppgc::WeakMember; struct ResourceDescriptor; @@ -97,14 +133,18 @@ int64_t local_bigint64_array_get(Isolate* isolate, const Local& array, size_t in uint64_t local_biguint64_array_get(Isolate* isolate, const Local& array, size_t index); // Global -void global_drop(Global value); +void global_reset(Global* value); Global global_clone(const Global& value); Local global_to_local(Isolate* isolate, const Global& value); -void global_make_weak( - Isolate* isolate, Global* value, size_t /* void* */ data, WeakCallback callback); + +// TracedReference (cppgc/Oilpan) +TracedReference traced_reference_from_local(Isolate* isolate, Local value); +Local traced_reference_to_local(Isolate* isolate, const TracedReference& value); +void traced_reference_reset(TracedReference* value); +bool traced_reference_is_empty(const TracedReference& value); // Wrappers -Local wrap_resource(Isolate* isolate, size_t resource, const Global& tmpl, size_t drop_callback); +Local wrap_resource(Isolate* isolate, size_t resource, const Global& tmpl); // Unwrappers ::rust::String unwrap_string(Isolate* isolate, Local value); @@ -155,4 +195,41 @@ void isolate_throw_exception(Isolate* isolate, Local exception); void isolate_throw_error(Isolate* isolate, ::rust::Str message); bool isolate_is_locked(Isolate* isolate); +// Oilpan +RustResource* cppgc_make_garbage_collected(Isolate* isolate, size_t size, size_t alignment); +size_t cppgc_rust_resource_size(); +uintptr_t* cppgc_rust_resource_data(RustResource* resource); +const uintptr_t* cppgc_rust_resource_data_const(const RustResource* resource); +void cppgc_visitor_trace(CppgcVisitor* visitor, const TracedReference& handle); +void cppgc_visitor_trace_member(CppgcVisitor* visitor, size_t member_storage); +void cppgc_visitor_trace_weak_member(CppgcVisitor* visitor, size_t weak_member_storage); + +// cppgc - persistent +size_t cppgc_persistent_size(); +void cppgc_persistent_construct(size_t storage, RustResource* resource); +void cppgc_persistent_destruct(size_t storage); +RustResource* cppgc_persistent_get(size_t storage); +void cppgc_persistent_assign(size_t storage, RustResource* resource); + +// cppgc - weak persistent +size_t cppgc_weak_persistent_size(); +void cppgc_weak_persistent_construct(size_t storage, RustResource* resource); +void cppgc_weak_persistent_destruct(size_t storage); +RustResource* cppgc_weak_persistent_get(size_t storage); +void cppgc_weak_persistent_assign(size_t storage, RustResource* resource); + +// cppgc - member +size_t cppgc_member_size(); +void cppgc_member_construct(size_t storage, RustResource* resource); +void cppgc_member_destruct(size_t storage); +RustResource* cppgc_member_get(size_t storage); +void cppgc_member_assign(size_t storage, RustResource* resource); + +// cppgc - weak member +size_t cppgc_weak_member_size(); +void cppgc_weak_member_construct(size_t storage, RustResource* resource); +void cppgc_weak_member_destruct(size_t storage); +RustResource* cppgc_weak_member_get(size_t storage); +void cppgc_weak_member_assign(size_t storage, RustResource* resource); + } // namespace workerd::rust::jsg diff --git a/src/rust/jsg/jsg.h b/src/rust/jsg/jsg.h index a916c1c8ff0..ecc363ec3ac 100644 --- a/src/rust/jsg/jsg.h +++ b/src/rust/jsg/jsg.h @@ -20,6 +20,19 @@ struct RustModuleRegistry: public ::workerd::rust::jsg::ModuleRegistry { void addBuiltinModule( ::rust::Str specifier, ModuleCallback moduleCallback, ModuleType moduleType) override { + ::workerd::jsg::ModuleType jsgModuleType; + switch (moduleType) { + case ModuleType::Bundle: + jsgModuleType = ::workerd::jsg::ModuleType::BUNDLE; + break; + case ModuleType::Builtin: + jsgModuleType = ::workerd::jsg::ModuleType::BUILTIN; + break; + case ModuleType::Internal: + jsgModuleType = ::workerd::jsg::ModuleType::INTERNAL; + break; + } + registry.addBuiltinModule(kj::str(specifier), [kj_specifier = kj::str(specifier), callback = kj::mv(moduleCallback)]( ::workerd::jsg::Lock& js, ::workerd::jsg::ModuleRegistry::ResolveMethod, @@ -34,7 +47,7 @@ struct RustModuleRegistry: public ::workerd::rust::jsg::ModuleRegistry { return kj::Maybe( ModuleInfo(js, kj_specifier, kj::none, ObjectModuleInfo(js, value.As()))); }, - moduleType); + jsgModuleType); } Registry& registry; diff --git a/src/rust/jsg/lib.rs b/src/rust/jsg/lib.rs index f3eaa15d244..7490e737df7 100644 --- a/src/rust/jsg/lib.rs +++ b/src/rust/jsg/lib.rs @@ -1,28 +1,36 @@ -use std::cell::UnsafeCell; +use std::any::Any; +use std::any::TypeId; +use std::collections::HashMap; use std::future::Future; use std::num::ParseIntError; use std::ops::Deref; -use std::ops::DerefMut; -use std::os::raw::c_void; -use std::ptr::NonNull; -use std::rc::Rc; use kj_rs::KjMaybe; pub mod modules; +pub mod resource; pub mod v8; mod wrappable; +pub use resource::Ref; +pub use resource::ResourceImpl; +pub use resource::WeakRef; pub use v8::BigInt64Array; pub use v8::BigUint64Array; pub use v8::Float32Array; pub use v8::Float64Array; +pub use v8::GarbageCollected; +pub use v8::GcParent; +pub use v8::GcVisitor; pub use v8::Int8Array; pub use v8::Int16Array; pub use v8::Int32Array; +pub use v8::IsolatePtr; +pub use v8::TracedReference; pub use v8::Uint8Array; pub use v8::Uint16Array; pub use v8::Uint32Array; +pub use v8::cppgc; pub use v8::ffi::ExceptionType; pub use wrappable::FromJS; pub use wrappable::ToJS; @@ -91,67 +99,13 @@ fn get_resource_descriptor() -> v8::ffi::ResourceDescriptor { pub fn create_resource_constructor( lock: &mut Lock, ) -> v8::Global { + // SAFETY: Lock guarantees the isolate is valid and locked unsafe { v8::ffi::create_resource_template(lock.isolate().as_ffi(), &get_resource_descriptor::()) .into() } } -/// Wraps a Rust resource for exposure to JavaScript. -/// -/// # Safety -/// The caller must ensure V8 operations are performed within the correct isolate/context and -/// that the resource's lifetime is properly managed via the Realm. -pub unsafe fn wrap_resource<'a, R: Resource + 'a, RT: ResourceTemplate>( - lock: &mut Lock, - mut resource: Ref, - resource_template: &mut RT, -) -> v8::Local<'a, v8::Value> { - match resource - .get_state() - .strong_wrapper - .as_ref() - .map(|val| val.as_local(lock)) - { - Some(value) if value.has_value() => value.into(), - _ => { - let constructor = resource_template.get_constructor(); - - // Store the leaked Ref in ResourceState.this and the drop function - let drop_fn = (*resource).get_drop_fn(); - resource.get_state().this = Ref::into_raw(resource.clone()).cast(); - resource.get_state().drop_fn = Some(drop_fn); - - let instance: v8::Local<'a, v8::Value> = unsafe { - v8::Local::from_ffi( - lock.isolate(), - v8::ffi::wrap_resource( - lock.isolate().as_ffi(), - resource.get_state().this as usize, - constructor.as_ffi_ref(), - drop_fn as usize, - ), - ) - }; - unsafe { - resource - .get_state() - .attach_wrapper(lock.realm(), instance.clone().into()); - } - instance - } - } -} - -pub fn unwrap_resource<'a, R: Resource>( - lock: &'a mut Lock, - value: v8::Local, -) -> &'a mut R { - let ptr = - unsafe { v8::ffi::unwrap_resource(lock.isolate().as_ffi(), value.into_ffi()) as *mut R }; - unsafe { &mut *ptr } -} - impl From<&str> for ExceptionType { fn from(value: &str) -> Self { match value { @@ -613,7 +567,13 @@ impl Lock { self.isolate } + pub fn is_locked(&self) -> bool { + // SAFETY: Lock guarantees the isolate is valid + unsafe { self.isolate.is_locked() } + } + pub fn new_object<'a>(&mut self) -> v8::Local<'a, v8::Object> { + // SAFETY: Lock guarantees the isolate is valid and locked unsafe { v8::Local::from_ffi( self.isolate(), @@ -622,6 +582,11 @@ impl Lock { } } + pub fn throw_error(&mut self, message: &str) { + // SAFETY: Lock guarantees the isolate is valid and locked + unsafe { v8::ffi::isolate_throw_error(self.isolate().as_ffi(), message) } + } + pub fn await_io(self, _fut: F, _callback: C) -> Result where F: Future, @@ -630,7 +595,7 @@ impl Lock { todo!() } - fn realm(&mut self) -> &mut Realm { + pub fn realm(&mut self) -> &mut Realm { unsafe { &mut *crate::ffi::realm_from_isolate(self.isolate().as_ffi()) } } @@ -645,67 +610,6 @@ impl Lock { } } -/// This is analogous to `jsg::Ref` in C++ JSG. -/// -/// # Thread Safety -/// -/// **`Ref` is not thread-safe and must not be sent or shared across threads.** -/// Resources managed by `Ref` are bound to the thread of the V8 isolate in which they were created. -/// Attempting to send or access a `Ref` from another thread is undefined behavior. -/// This is enforced by the use of `Rc` and `UnsafeCell`, which are not `Send` or `Sync`. -pub struct Ref { - val: Rc>, -} - -impl Deref for Ref { - type Target = T; - - fn deref(&self) -> &Self::Target { - let ptr = self.val.get(); - unsafe { &*ptr } - } -} - -impl DerefMut for Ref { - fn deref_mut(&mut self) -> &mut Self::Target { - let ptr = self.val.get(); - unsafe { &mut *ptr } - } -} - -impl Ref { - pub fn new(t: T) -> Self { - Self { - val: Rc::new(UnsafeCell::new(t)), - } - } - - pub fn into_raw(r: Self) -> *mut T { - UnsafeCell::raw_get(Rc::into_raw(r.val)) - } - - /// Reconstructs a `Ref` from a raw pointer. - /// - /// # Safety - /// The caller must ensure: - /// - `this` is a valid pointer that was previously created by `Ref::into_raw()` - /// - This pointer has not been used to reconstruct a `Ref` that is still alive - /// - The pointer is properly aligned and points to a valid `T` instance - pub unsafe fn from_raw(this: *mut T) -> Self { - Self { - val: unsafe { Rc::from_raw(this.cast::>()) }, - } - } -} - -impl Clone for Ref { - fn clone(&self) -> Self { - Self { - val: self.val.clone(), - } - } -} - /// Provides metadata about Rust types exposed to JavaScript. /// /// This trait provides type information used for error messages, memory tracking, @@ -751,93 +655,54 @@ pub enum Member { }, } -/// Tracks the V8 wrapper object for a Rust resource. -/// -/// Each Resource embeds a `ResourceState` to maintain the connection between the Rust object and -/// its JavaScript wrapper. When a resource is wrapped, the `ResourceState` stores a weak V8 handle -/// to the wrapper object along with pointers needed for cleanup: the leaked `Ref` pointer -/// and the drop function to reconstruct it. The Realm uses this state to perform deterministic -/// cleanup when the context is disposed. -pub struct ResourceState { - pub this: *mut c_void, - pub drop_fn: Option, - pub strong_wrapper: Option>, - pub isolate: Option, -} - -impl Default for ResourceState { - fn default() -> Self { - Self { - this: std::ptr::null_mut(), - drop_fn: None, - strong_wrapper: None, - isolate: None, - } - } -} - -impl ResourceState { - /// Attaches a V8 wrapper object to this resource state. - /// - /// # Safety - /// The caller must ensure: - /// - `object` is a valid V8 local object handle - /// - This method is called from the correct V8 isolate/context - /// - `self.this` pointer remains valid until either the weak callback fires or `Realm::drop()` is called - /// - /// # Panics - /// Panics if a wrapper has already been attached (i.e., `strong_wrapper` is not `None`). - pub unsafe fn attach_wrapper(&mut self, realm: &mut Realm, object: v8::Local) { - assert!(self.strong_wrapper.is_none()); - - self.strong_wrapper = Some(object.into()); - self.isolate = Some(realm.isolate()); - - realm.add_resource(NonNull::from(&mut *self)); - - let Some(wrapper) = self.strong_wrapper.as_mut() else { - unreachable!("This should not happen") - }; - let isolate = self.isolate.expect("isolate should be set"); - unsafe { - wrapper.make_weak(isolate, self.this, Self::weak_callback); - } - } - - fn weak_callback(_isolate: *mut v8::ffi::Isolate, _data: usize) { - // TODO: Implement GC-based cleanup for resources that outlive their context. - // Current resources like DnsUtil don't require GC cleanup. - // All cleanup happens deterministically in Realm::drop() during context disposal. - } -} - /// Rust types exposed to JavaScript as resource types. /// /// Resource types are passed by reference and call back into Rust when JavaScript accesses /// their members. This is analogous to `JSG_RESOURCE_TYPE` in C++ JSG. Resources must provide /// member declarations, a cleanup function for GC, and access to their V8 wrapper state. -pub trait Resource: Type { +pub trait Resource: Type + GarbageCollected + Sized + 'static { + type Template: ResourceTemplate; + /// Returns the list of methods, properties, and constructors exposed to JavaScript. fn members() -> Vec where Self: Sized; - /// Returns the cleanup function called when V8 GC collects the wrapper or the context is - /// disposed. This function reconstructs the leaked `Ref` and drops it. - fn get_drop_fn(&self) -> unsafe extern "C" fn(*mut v8::ffi::Isolate, *mut c_void); + /// Allocates a resource instance on the cppgc heap and returns a persistent handle. + /// + /// The returned `Ref` manages the resource's lifetime. When all `Ref` handles are + /// dropped, the instance becomes eligible for garbage collection. + fn alloc(lock: &mut Lock, this: Self) -> Ref { + resource::Instance::alloc(lock, this) + } - /// Returns mutable access to the `ResourceState` tracking this resource's V8 wrapper. - fn get_state(&mut self) -> &mut ResourceState; + /// Wraps a resource reference for exposure to JavaScript. + fn wrap<'a>(resource: Ref, lock: &mut Lock) -> v8::Local<'a, v8::Value> + where + Self::Template: 'static, + { + resource::wrap(lock, resource) + } + + /// Unwraps a JavaScript value to get a reference to the underlying resource. + /// + /// # Safety + /// The `value` must be a valid JavaScript wrapper for this resource type. + fn unwrap<'a>(lock: &'a mut Lock, value: v8::Local) -> &'a mut Self { + unsafe { resource::unwrap(lock, value) } + } } /// Caches the V8 `FunctionTemplate` for a resource type. /// -/// A `ResourceTemplate` is created per Lock and caches the V8 function template used to +/// A `ResourceTemplate` is created per Realm and caches the V8 function template used to /// instantiate JavaScript wrappers for a resource type. This avoids recreating the template /// on every wrap operation. pub trait ResourceTemplate { /// Creates a new template for the given lock, initializing the V8 function template. - fn new(lock: &mut Lock) -> Self; + fn new(lock: &mut Lock) -> Self + where + Self: Sized; /// Returns the cached V8 function template used to create wrappers. fn get_constructor(&self) -> &v8::Global; @@ -849,33 +714,16 @@ pub trait ResourceTemplate { /// further Rust involvement after wrapping. This is analogous to `JSG_STRUCT` in C++ JSG. pub trait Struct: Type {} -/// Drops a resource by reconstructing it from a raw pointer and dropping it. -/// This function is typically used as a callback when V8 garbage collects a wrapped object. -/// -/// # Safety -/// The caller must ensure: -/// - `this` is a valid pointer to a resource of type `R` that was previously created by `Ref::into_raw()` -/// - This function is only called once per resource -/// - The resource has not already been dropped -pub unsafe fn drop_resource(_isolate: *mut ffi::Isolate, this: *mut c_void) { - let this = this.cast::(); - let this = unsafe { Ref::from_raw(this) }; - drop(this); -} - -/// Tracks the lifetime of Rust resources exposed to a V8 context. +/// Per-isolate state for Rust resources exposed to JavaScript. /// -/// When Rust resources are wrapped for JavaScript, they are leaked via `Ref::into_raw()` to -/// create stable pointers that outlive V8's GC. The Realm tracks all such leaked resources -/// and ensures deterministic cleanup when the V8 context is disposed. -/// -/// A Realm is created per V8 context and stored in the context's embedder data. Resources -/// register themselves via `add_resource()` during wrapping. When the context is torn down, -/// `Drop` iterates all tracked resources and calls their drop functions to reconstruct and -/// free any leaked `Ref` values for wrappers not yet collected by V8's GC. +/// A Realm is created for each V8 isolate and stored in the isolate's data slot. It holds +/// cached function templates and tracks resource instances. When all Rust `Ref` handles to a +/// resource are dropped, the V8 wrapper becomes weak and V8 GC will trigger cleanup via the +/// weak callback. pub struct Realm { isolate: v8::IsolatePtr, - resources: Vec<*mut ResourceState>, + /// Resource templates keyed by `TypeId`. + templates: HashMap>, } impl Realm { @@ -883,17 +731,28 @@ impl Realm { pub fn from_isolate(isolate: v8::IsolatePtr) -> Self { Self { isolate, - resources: Vec::new(), + templates: HashMap::default(), } } - pub fn add_resource(&mut self, resource: NonNull) { - self.resources.push(resource.as_ptr()); - } - pub fn isolate(&self) -> v8::IsolatePtr { self.isolate } + + /// Gets or creates the resource tracking structure for a given resource type. + /// + /// # Panics + /// Panics if type mismatch (indicates a bug). + pub fn get_resources(&mut self) -> &mut ResourceImpl + where + ::Template: 'static, + { + self.templates + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(ResourceImpl::::default())) + .downcast_mut::>() + .expect("Template type mismatch") + } } impl Drop for Realm { @@ -902,28 +761,6 @@ impl Drop for Realm { unsafe { self.isolate.is_locked() }, "Realm must be dropped while holding the isolate lock" ); - - // Clean up all leaked Refs during deterministic context disposal. - // Each resource_ptr points to a ResourceState embedded in a Resource. - // When wrapping a resource, we leak a Ref and store the raw pointer - // in ResourceState.this. If strong_wrapper is still Some, the V8 object - // wasn't GC'd, so we must manually drop the leaked Ref by calling drop_fn. - for resource_ptr in &self.resources { - unsafe { - let resource_state = &**resource_ptr; - // Only drop if the wrapper hasn't been collected by V8's GC - if resource_state.strong_wrapper.is_some() - && let Some(drop_fn) = resource_state.drop_fn - && !resource_state.this.is_null() - { - // Note: Do not access resource_state after drop_fn returns, as - // ResourceState is embedded inside the Resource which is now freed. - let isolate = resource_state.isolate.expect("isolate should be set"); - drop_fn(isolate.as_ffi(), resource_state.this); - } - } - } - self.resources.clear(); } } diff --git a/src/rust/jsg/resource.rs b/src/rust/jsg/resource.rs new file mode 100644 index 00000000000..140334aaeef --- /dev/null +++ b/src/rust/jsg/resource.rs @@ -0,0 +1,367 @@ +use std::cell::Cell; +use std::cell::UnsafeCell; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::NonNull; + +use crate::GarbageCollected; +use crate::Lock; +use crate::Resource; +use crate::ResourceTemplate; +use crate::v8; + +pub(crate) struct Instance { + pub resource: R, + wrapper: Option>, + strong_refcount: Cell, +} + +impl Instance { + pub fn alloc(lock: &mut Lock, resource: R) -> Ref { + let instance = Self { + resource, + wrapper: None, + strong_refcount: Cell::new(0), + }; + let ptr = unsafe { v8::cppgc::make_garbage_collected(lock.isolate(), instance) }; + Ref::from_ptr(&ptr) + } + + pub(crate) fn traced_reference(&self) -> Option<&v8::TracedReference> { + self.wrapper.as_ref() + } + + pub(crate) fn set_wrapper(&mut self, object: v8::Local) { + debug_assert!(self.wrapper.is_none()); + self.wrapper = Some(object.into()); + } + + fn increment_ref(&self) { + self.strong_refcount.set(self.strong_refcount.get() + 1); + } + + fn decrement_ref(&self) { + let count = self.strong_refcount.get(); + debug_assert!(count > 0); + self.strong_refcount.set(count - 1); + } +} + +impl v8::GcParent for Instance { + fn strong_refcount(&self) -> u32 { + self.strong_refcount.get() + } + + fn has_wrapper(&self) -> bool { + self.wrapper.is_some() + } +} + +impl GarbageCollected for Instance { + fn trace(&self, visitor: &mut v8::GcVisitor<'_>) { + let mut child_visitor = visitor.with_parent(self); + self.resource.trace(&mut child_visitor); + + if let Some(wrapper) = &self.wrapper { + visitor.trace(wrapper); + } + } + + fn get_name(&self) -> Option<&'static std::ffi::CStr> { + None + } +} + +/// A reference to a resource that dynamically switches between strong and traced modes. +pub struct Ref { + storage: UnsafeCell, + _marker: PhantomData, +} + +enum RefStorage { + Strong(v8::cppgc::Handle), + Traced(v8::cppgc::Member), +} + +impl RefStorage { + fn get(&self) -> Option> { + match self { + Self::Strong(handle) => handle.get_resource(), + Self::Traced(member) => member.get(), + } + } +} + +impl Ref { + fn from_ptr(ptr: &v8::cppgc::Ptr>) -> Self { + ptr.get().increment_ref(); + Self { + storage: UnsafeCell::new(RefStorage::Strong(unsafe { + v8::cppgc::Handle::from_resource(ptr.as_rust_resource()) + })), + _marker: PhantomData, + } + } + + pub(crate) fn instance_ptr(&self) -> *mut Instance { + // SAFETY: single-threaded V8 isolate + unsafe { &*self.storage.get() } + .get() + .map(|r| unsafe { v8::cppgc::rust_resource_to_instance::>(r.as_ptr()) }) + .expect("Ref handle should always be valid") + } + + fn instance(&self) -> &Instance { + unsafe { &*self.instance_ptr() } + } + + pub(crate) fn rust_resource(&self) -> Option> { + // SAFETY: single-threaded V8 isolate + unsafe { &*self.storage.get() }.get() + } + + pub(crate) fn visit(&self, visitor: &mut v8::GcVisitor<'_>) -> &Instance { + let instance = self.instance(); + + if let Some(parent) = visitor.parent() { + let should_be_strong = parent.strong_refcount() > 0 && !parent.has_wrapper(); + + // SAFETY: single-threaded V8 isolate + unsafe { + match &*self.storage.get() { + RefStorage::Traced(member) if should_be_strong => { + if let Some(resource) = member.get() { + instance.increment_ref(); + *self.storage.get() = RefStorage::Strong( + v8::cppgc::Handle::from_resource(resource.as_ptr()), + ); + } + } + RefStorage::Strong(handle) if !should_be_strong => { + if let Some(resource) = handle.get_resource() { + instance.decrement_ref(); + *self.storage.get() = + RefStorage::Traced(v8::cppgc::Member::from_resource(resource)); + } + } + _ => {} + } + } + } + + // SAFETY: single-threaded V8 isolate + unsafe { + if let RefStorage::Traced(member) = &*self.storage.get() { + visitor.trace_member(member); + } + } + + instance + } +} + +impl Deref for Ref { + type Target = R; + + fn deref(&self) -> &Self::Target { + // SAFETY: Ref maintains invariant that handle is valid + unsafe { &(*self.instance_ptr()).resource } + } +} + +impl Clone for Ref { + fn clone(&self) -> Self { + self.instance().increment_ref(); + Self { + storage: UnsafeCell::new(RefStorage::Strong( + self.rust_resource() + .map(|r| unsafe { v8::cppgc::Handle::from_resource(r.as_ptr()) }) + .unwrap_or_default(), + )), + _marker: PhantomData, + } + } +} + +impl Drop for Ref { + fn drop(&mut self) { + if let RefStorage::Strong(handle) = self.storage.get_mut() + && let Some(r) = handle.get_resource() + { + let instance = + unsafe { v8::cppgc::rust_resource_to_instance::>(r.as_ptr()) }; + unsafe { (*instance).decrement_ref() }; + } + } +} + +/// A weak reference to a resource that doesn't prevent garbage collection. +pub struct WeakRef { + weak_handle: v8::cppgc::WeakHandle, + _marker: PhantomData, +} + +impl WeakRef { + pub fn get(&self) -> Option<&R> { + self.weak_handle.get().map(|r| { + let instance_ptr = + unsafe { v8::cppgc::rust_resource_to_instance::>(r.as_ptr()) }; + unsafe { &(*instance_ptr).resource } + }) + } + + pub fn is_alive(&self) -> bool { + self.weak_handle.is_alive() + } + + pub fn upgrade(&self) -> Option> { + self.weak_handle.get().map(|r| { + let instance_ptr = + unsafe { v8::cppgc::rust_resource_to_instance::>(r.as_ptr()) }; + unsafe { (*instance_ptr).increment_ref() }; + + Ref { + storage: UnsafeCell::new(RefStorage::Strong(unsafe { + v8::cppgc::Handle::from_resource(r.as_ptr()) + })), + _marker: PhantomData, + } + }) + } +} + +impl From<&Ref> for WeakRef { + fn from(r: &Ref) -> Self { + Self { + weak_handle: r + .rust_resource() + .map(|rr| unsafe { v8::cppgc::WeakHandle::from_resource(rr.as_ptr()) }) + .unwrap_or_default(), + _marker: PhantomData, + } + } +} + +impl Clone for WeakRef { + fn clone(&self) -> Self { + Self { + weak_handle: self + .weak_handle + .get() + .map(|r| unsafe { v8::cppgc::WeakHandle::from_resource(r.as_ptr()) }) + .unwrap_or_default(), + _marker: PhantomData, + } + } +} + +impl Default for WeakRef { + fn default() -> Self { + Self { + weak_handle: v8::cppgc::WeakHandle::default(), + _marker: PhantomData, + } + } +} + +#[expect(clippy::needless_pass_by_value)] +pub fn wrap<'a, R: Resource + 'static>( + lock: &mut Lock, + resource: Ref, +) -> v8::Local<'a, v8::Value> +where + ::Template: 'static, +{ + let instance_ptr = resource.instance_ptr(); + // SAFETY: Ref guarantees instance is valid + let instance = unsafe { &mut *instance_ptr }; + + if let Some(traced) = instance.traced_reference() { + let local = traced.get(lock); + if local.has_value() { + return local.into(); + } + } + + // Get isolate early since it's Copy and we need it multiple times + let isolate = lock.isolate(); + + // SAFETY: We need to work around the borrow checker here because: + // 1. realm().get_resources() requires &mut Lock via realm() + // 2. get_constructor() also requires &mut Lock + // Both operations are safe because they don't actually overlap in their mutations. + let constructor = unsafe { + let lock_ptr = lock as *mut Lock; + let resources = (*lock_ptr).realm().get_resources::(); + resources.get_constructor(&mut *lock_ptr) + }; + + let wrapped: v8::Local<'a, v8::Value> = unsafe { + v8::Local::from_ffi( + isolate, + v8::ffi::wrap_resource( + isolate.as_ffi(), + instance_ptr as usize, + constructor.as_ffi_ref(), + ), + ) + }; + + instance.set_wrapper(wrapped.clone().into()); + + wrapped +} + +/// # Safety +/// The `value` must be a valid JavaScript wrapper for type `R`. +pub unsafe fn unwrap<'a, R: Resource>( + lock: &'a mut Lock, + value: v8::Local, +) -> &'a mut R { + let ptr = unsafe { + v8::ffi::unwrap_resource(lock.isolate().as_ffi(), value.into_ffi()) as *mut Instance + }; + unsafe { &mut (*ptr).resource } +} + +/// # Safety +/// The `value` must be a valid JavaScript wrapper for type `R`. +pub unsafe fn unwrap_ref(lock: &mut Lock, value: v8::Local) -> Ref { + // SAFETY: Caller guarantees value wraps an Instance + let instance_ptr = unsafe { + v8::ffi::unwrap_resource(lock.isolate().as_ffi(), value.into_ffi()) as *mut Instance + }; + + unsafe { (*instance_ptr).increment_ref() }; + + let rust_resource = unsafe { v8::cppgc::instance_to_rust_resource(instance_ptr) }; + + Ref { + storage: UnsafeCell::new(RefStorage::Strong(unsafe { + v8::cppgc::Handle::from_resource(rust_resource) + })), + _marker: PhantomData, + } +} + +pub struct ResourceImpl { + template: Option, + _marker: PhantomData, +} + +impl Default for ResourceImpl { + fn default() -> Self { + Self { + template: None, + _marker: PhantomData, + } + } +} + +impl ResourceImpl { + pub fn get_constructor(&mut self, lock: &mut Lock) -> &v8::Global { + self.template + .get_or_insert_with(|| R::Template::new(lock)) + .get_constructor() + } +} diff --git a/src/rust/jsg/v8.rs b/src/rust/jsg/v8.rs index 37545bb94f9..6c2805a21ee 100644 --- a/src/rust/jsg/v8.rs +++ b/src/rust/jsg/v8.rs @@ -1,4 +1,27 @@ -use core::ffi::c_void; +//! V8 JavaScript engine bindings and garbage collector integration. +//! +//! This module provides Rust wrappers for V8 types and the cppgc (Oilpan) garbage collector, +//! enabling safe interop between Rust and V8's JavaScript runtime. +//! +//! # Core Types +//! +//! - [`IsolatePtr`] - Safe wrapper around `v8::Isolate*`, the V8 runtime instance +//! - [`Local<'a, T>`] - Stack-allocated handle to a V8 value, tied to a `HandleScope` +//! - [`Global`] - Persistent handle that outlives `HandleScope`s +//! - [`TracedReference`] - Handle traced by the garbage collector for cppgc objects +//! +//! # Garbage Collection +//! +//! The [`cppgc`] submodule provides Rust wrappers for V8's C++ garbage collector types: +//! +//! - [`cppgc::Handle`] - Strong persistent reference (off-heap → on-heap) +//! - [`cppgc::WeakHandle`] - Weak persistent reference (off-heap → on-heap) +//! - [`cppgc::Member`] - Strong member reference (on-heap → on-heap, needs tracing) +//! - [`cppgc::WeakMember`] - Weak member reference (on-heap → on-heap, needs tracing) +//! +//! Resources implement [`GarbageCollected`] to trace their V8 handles during GC. + +use std::ffi::c_char; use std::fmt::Display; use std::marker::PhantomData; use std::ptr::NonNull; @@ -20,6 +43,14 @@ pub mod ffi { ptr: usize, } + struct TracedReference { + ptr: usize, + } + + struct CppgcVisitor { + ptr: usize, + } + #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum ExceptionType { OperationError, @@ -40,17 +71,18 @@ pub mod ffi { ReferenceError, } - /// Module visibility level, corresponds to `workerd::jsg::ModuleType` from modules.capnp. - /// Values are automatically assigned by `cxx` because of extern declaration below. - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - #[repr(u16)] - enum ModuleType { - BUNDLE, - BUILTIN, - INTERNAL, - } - unsafe extern "C++" { - type ModuleType; + extern "Rust" { + /// Called from C++ RustResource destructor to drop the Rust object. + /// The `resource` pointer contains a fat pointer (data[2]) to `dyn GarbageCollected`. + unsafe fn cppgc_invoke_drop(resource: *mut RustResource); + + /// Called from C++ RustResource::Trace to trace nested handles. + /// The `resource` pointer contains a fat pointer (data[2]) to `dyn GarbageCollected`. + unsafe fn cppgc_invoke_trace(resource: *const RustResource, visitor: *mut CppgcVisitor); + + /// Called from C++ RustResource::GetHumanReadableName. + /// Returns null if no name is available, otherwise a static string. + unsafe fn cppgc_invoke_get_name(resource: *const RustResource) -> *const c_char; } unsafe extern "C++" { @@ -58,6 +90,11 @@ pub mod ffi { type Isolate; type FunctionCallbackInfo; + type RustResource; + type CppgcPersistent; + type CppgcWeakPersistent; + type CppgcMember; + type CppgcWeakMember; // Local pub unsafe fn local_drop(value: Local); @@ -228,15 +265,21 @@ pub mod ffi { ) -> u64; // Global - pub unsafe fn global_drop(value: Global); + pub unsafe fn global_reset(value: *mut Global); pub unsafe fn global_clone(value: &Global) -> Global; pub unsafe fn global_to_local(isolate: *mut Isolate, value: &Global) -> Local; - pub unsafe fn global_make_weak( + + // TracedReference (cppgc/Oilpan) + pub unsafe fn traced_reference_from_local( isolate: *mut Isolate, - value: *mut Global, - data: usize, /* void* */ - callback: unsafe fn(isolate: *mut Isolate, data: usize) -> (), - ); + value: Local, + ) -> TracedReference; + pub unsafe fn traced_reference_to_local( + isolate: *mut Isolate, + value: &TracedReference, + ) -> Local; + pub unsafe fn traced_reference_reset(value: *mut TracedReference); + pub unsafe fn traced_reference_is_empty(value: &TracedReference) -> bool; // Unwrappers pub unsafe fn unwrap_string(isolate: *mut Isolate, value: Local) -> String; @@ -271,6 +314,46 @@ pub mod ffi { pub unsafe fn isolate_throw_exception(isolate: *mut Isolate, exception: Local); pub unsafe fn isolate_throw_error(isolate: *mut Isolate, message: &str); pub unsafe fn isolate_is_locked(isolate: *mut Isolate) -> bool; + + // cppgc - Allocate Rust objects directly on the GC heap + pub unsafe fn cppgc_make_garbage_collected( + isolate: *mut Isolate, + size: usize, + alignment: usize, + ) -> *mut RustResource; + pub fn cppgc_rust_resource_size() -> usize; + pub unsafe fn cppgc_rust_resource_data(resource: *mut RustResource) -> *mut usize; + pub unsafe fn cppgc_rust_resource_data_const(resource: *const RustResource) + -> *const usize; + pub unsafe fn cppgc_visitor_trace(visitor: *mut CppgcVisitor, handle: &TracedReference); + pub unsafe fn cppgc_visitor_trace_member(visitor: *mut CppgcVisitor, member_storage: usize); + pub unsafe fn cppgc_visitor_trace_weak_member( + visitor: *mut CppgcVisitor, + weak_member_storage: usize, + ); + pub fn cppgc_persistent_size() -> usize; + pub unsafe fn cppgc_persistent_construct(storage: usize, resource: *mut RustResource); + pub unsafe fn cppgc_persistent_destruct(storage: usize); + pub unsafe fn cppgc_persistent_get(storage: usize) -> *mut RustResource; + pub unsafe fn cppgc_persistent_assign(storage: usize, resource: *mut RustResource); + // WeakPersistent inline storage functions + pub fn cppgc_weak_persistent_size() -> usize; + pub unsafe fn cppgc_weak_persistent_construct(storage: usize, resource: *mut RustResource); + pub unsafe fn cppgc_weak_persistent_destruct(storage: usize); + pub unsafe fn cppgc_weak_persistent_get(storage: usize) -> *mut RustResource; + pub unsafe fn cppgc_weak_persistent_assign(storage: usize, resource: *mut RustResource); + // Member inline storage functions + pub fn cppgc_member_size() -> usize; + pub unsafe fn cppgc_member_construct(storage: usize, resource: *mut RustResource); + pub unsafe fn cppgc_member_destruct(storage: usize); + pub unsafe fn cppgc_member_get(storage: usize) -> *mut RustResource; + pub unsafe fn cppgc_member_assign(storage: usize, resource: *mut RustResource); + // WeakMember inline storage functions + pub fn cppgc_weak_member_size() -> usize; + pub unsafe fn cppgc_weak_member_construct(storage: usize, resource: *mut RustResource); + pub unsafe fn cppgc_weak_member_destruct(storage: usize); + pub unsafe fn cppgc_weak_member_get(storage: usize) -> *mut RustResource; + pub unsafe fn cppgc_weak_member_assign(storage: usize, resource: *mut RustResource); } pub struct ConstructorDescriptor { @@ -305,7 +388,6 @@ pub mod ffi { isolate: *mut Isolate, resource: usize, /* R* */ constructor: &Global, /* v8::Global */ - drop_callback: usize, /* R* -> () */ ) -> Local /* v8::Local */; pub unsafe fn unwrap_resource( @@ -314,6 +396,16 @@ pub mod ffi { ) -> usize /* R* */; } + /// Module visibility level, mirroring workerd::jsg::ModuleType from modules.capnp. + /// + /// CXX shared enums cannot reference existing C++ enums, so we define matching values here. + /// The conversion to workerd::jsg::ModuleType happens in jsg.h's RustModuleRegistry. + enum ModuleType { + Bundle = 0, + Builtin = 1, + Internal = 2, + } + unsafe extern "C++" { type ModuleRegistry; @@ -1037,6 +1129,14 @@ impl Global { &self.handle } + /// Returns a mutable reference to the underlying FFI handle. + /// + /// # Safety + /// The caller must ensure the returned reference is not used after this `Global` is dropped. + pub unsafe fn as_ffi_mut(&mut self) -> *mut ffi::Global { + &raw mut self.handle + } + pub fn as_local<'a>(&self, lock: &mut Lock) -> Local<'a, T> { unsafe { Local::from_ffi( @@ -1046,27 +1146,13 @@ impl Global { } } - /// Makes this global handle weak, allowing V8 to garbage collect the object - /// and invoke the callback when the object is being collected. + /// Resets this global handle, releasing the persistent reference. /// /// # Safety - /// The caller must ensure: - /// - `isolate` is a valid V8 isolate wrapper - /// - `data` encodes a value that remains valid until the callback is invoked - /// - `callback` can safely handle the provided data value - pub unsafe fn make_weak( - &mut self, - isolate: IsolatePtr, - data: *mut c_void, - callback: fn(*mut ffi::Isolate, usize) -> (), - ) { + /// The caller must ensure the global handle is valid. + pub unsafe fn reset(&mut self) { unsafe { - ffi::global_make_weak( - isolate.as_ffi(), - &raw mut self.handle, - data as usize, - callback, - ); + ffi::global_reset(&raw mut self.handle); } } } @@ -1092,12 +1178,7 @@ impl From for Global { impl Drop for Global { fn drop(&mut self) { - let handle = ffi::Global { - ptr: self.handle.ptr, - }; - unsafe { - ffi::global_drop(handle); - } + unsafe { self.reset() }; } } @@ -1210,15 +1291,704 @@ impl<'a> FunctionCallbackInfo<'a> { } } +/// A reference to a V8 value that is traced by the garbage collector. +/// +/// Note: This type intentionally does NOT implement `Drop`. `TracedReference` is managed by V8's +/// traced handles infrastructure. During cppgc finalization, the traced handle memory may already +/// be freed, so calling `reset()` would cause a use-after-free. V8 automatically cleans up traced +/// references during GC. +pub struct TracedReference { + handle: ffi::TracedReference, + _marker: PhantomData, +} + +impl TracedReference { + pub fn is_empty(&self) -> bool { + unsafe { ffi::traced_reference_is_empty(&self.handle) } + } + + pub fn get<'a>(&self, lock: &mut Lock) -> Local<'a, T> { + unsafe { + Local::from_ffi( + lock.isolate(), + ffi::traced_reference_to_local(lock.isolate().as_ffi(), &self.handle), + ) + } + } + + pub fn reset(&mut self) { + unsafe { + ffi::traced_reference_reset(&raw mut self.handle); + } + } + + /// # Safety + /// The returned reference must not outlive this handle. + pub unsafe fn as_ffi_ref(&self) -> &ffi::TracedReference { + &self.handle + } +} + +impl From> for TracedReference { + fn from(local: Local<'_, T>) -> Self { + Self { + handle: unsafe { + ffi::traced_reference_from_local(local.isolate.as_ffi(), local.into_ffi()) + }, + _marker: PhantomData, + } + } +} + +/// A fat pointer to a `dyn GarbageCollected` trait object. +/// +/// This struct has the same memory layout as a Rust fat pointer: `[data_ptr, vtable_ptr]`. +/// It's used to store trait object pointers in C++ `RustResource::data[2]` and safely +/// reconstruct them on the Rust side. +/// +/// Uses `NonNull` internally to enforce that the pointers are never null. +#[repr(C)] +struct TraitObjectPtr { + data: NonNull<()>, + vtable: NonNull<()>, +} + +impl TraitObjectPtr { + /// Creates a `TraitObjectPtr` from a mutable reference to any `GarbageCollected` type. + fn from_ref(obj: &mut dyn GarbageCollected) -> Self { + // SAFETY: Transmute is safe here because we're converting a fat pointer to its raw + // representation which is guaranteed to be [data_ptr, vtable_ptr] for trait objects. + // References are always non-null, so NonNull is valid. + let [data, vtable] = + unsafe { std::mem::transmute::<*mut dyn GarbageCollected, [NonNull<()>; 2]>(obj) }; + Self { data, vtable } + } + + /// Reconstructs a shared reference from the stored fat pointer. + /// + /// # Safety + /// The original object must still be alive for lifetime `'a`. + #[expect(clippy::needless_lifetimes)] + unsafe fn as_ref<'a>(&'a self) -> &'a dyn GarbageCollected { + let fat_ptr = unsafe { + std::mem::transmute::<[NonNull<()>; 2], *const dyn GarbageCollected>([ + self.data, + self.vtable, + ]) + }; + unsafe { &*fat_ptr } + } + + /// Reconstructs a mutable reference from the stored fat pointer. + /// + /// # Safety + /// The original object must still be alive for lifetime `'a`, and no other references may exist. + #[expect(clippy::needless_lifetimes)] + unsafe fn as_mut<'a>(&'a mut self) -> &'a mut dyn GarbageCollected { + let fat_ptr = unsafe { + std::mem::transmute::<[NonNull<()>; 2], *mut dyn GarbageCollected>([ + self.data, + self.vtable, + ]) + }; + unsafe { &mut *fat_ptr } + } +} + +/// Returns a reference to the `TraitObjectPtr` stored in a `RustResource`. +/// +/// # Safety +/// The resource pointer must be valid and contain a properly initialized `TraitObjectPtr`. +unsafe fn get_trait_object_ptr(resource: *const ffi::RustResource) -> &'static TraitObjectPtr { + let data_ptr = unsafe { ffi::cppgc_rust_resource_data_const(resource) }; + unsafe { &*data_ptr.cast::() } +} + +/// Returns a mutable reference to the `TraitObjectPtr` stored in a `RustResource`. +/// +/// # Safety +/// The resource pointer must be valid and contain a properly initialized `TraitObjectPtr`. +unsafe fn get_trait_object_ptr_mut( + resource: *mut ffi::RustResource, +) -> &'static mut TraitObjectPtr { + let data_ptr = unsafe { ffi::cppgc_rust_resource_data(resource) }; + unsafe { &mut *data_ptr.cast::() } +} + +unsafe fn cppgc_invoke_drop(resource: *mut ffi::RustResource) { + let trait_ptr = unsafe { get_trait_object_ptr_mut(resource) }; + let obj = unsafe { trait_ptr.as_mut() }; + unsafe { std::ptr::drop_in_place(obj) }; +} + +unsafe fn cppgc_invoke_trace(resource: *const ffi::RustResource, visitor: *mut ffi::CppgcVisitor) { + let trait_ptr = unsafe { get_trait_object_ptr(resource) }; + let obj = unsafe { trait_ptr.as_ref() }; + let mut gc_visitor = unsafe { + GcVisitor::from_raw(ffi::CppgcVisitor { + ptr: (*visitor).ptr, + }) + }; + obj.trace(&mut gc_visitor); +} + +unsafe fn cppgc_invoke_get_name(resource: *const ffi::RustResource) -> *const c_char { + let trait_ptr = unsafe { get_trait_object_ptr(resource) }; + let obj = unsafe { trait_ptr.as_ref() }; + obj.get_name() + .map_or(std::ptr::null(), std::ffi::CStr::as_ptr) +} + +pub mod cppgc { + use std::cell::UnsafeCell; + use std::marker::PhantomData; + use std::ptr::NonNull; + + use super::GarbageCollected; + use super::IsolatePtr; + use super::ffi; + + const PERSISTENT_USIZE_COUNT: usize = 2; + const WEAK_PERSISTENT_USIZE_COUNT: usize = 2; + const MEMBER_USIZE_COUNT: usize = 1; + const WEAK_MEMBER_USIZE_COUNT: usize = 1; + + fn object_offset_for() -> usize { + let base = ffi::cppgc_rust_resource_size(); + let align = std::mem::align_of::(); + (base + align - 1) & !(align - 1) + } + + unsafe fn get_object_from_rust_resource( + rust_resource: *const ffi::RustResource, + ) -> *mut T { + unsafe { + rust_resource + .cast::() + .add(object_offset_for::()) + .cast::() + .cast_mut() + } + } + + /// # Safety + /// The isolate must be valid and locked by the current thread. + /// + /// # Panics + /// Panics if allocation fails (null pointer returned from cppgc). + pub unsafe fn make_garbage_collected( + isolate: IsolatePtr, + obj: T, + ) -> Ptr { + const { + assert!(std::mem::align_of::() <= 16); + } + + let base_size = ffi::cppgc_rust_resource_size(); + let aligned_offset = object_offset_for::(); + let additional_bytes = (aligned_offset - base_size) + std::mem::size_of::(); + + let pointer = unsafe { + ffi::cppgc_make_garbage_collected( + isolate.as_ffi(), + additional_bytes, + std::mem::align_of::(), + ) + }; + + assert!(!pointer.is_null(), "cppgc allocation failed"); + + unsafe { + let inner = get_object_from_rust_resource::(pointer); + inner.write(obj); + + // Store the fat pointer (data + vtable) in RustResource::data[2] + let data_ptr = ffi::cppgc_rust_resource_data(pointer); + let trait_ptr = super::TraitObjectPtr::from_ref(&mut *inner); + std::ptr::write(data_ptr.cast::(), trait_ptr); + } + + Ptr { + pointer: unsafe { NonNull::new_unchecked(pointer) }, + _phantom: PhantomData, + } + } + + /// # Safety + /// `rust_resource` must point to a valid `RustResource` containing an object of type T. + pub unsafe fn rust_resource_to_instance( + rust_resource: *mut ffi::RustResource, + ) -> *mut T { + unsafe { get_object_from_rust_resource::(rust_resource) } + } + + /// # Safety + /// `instance` must point to a valid object allocated via `make_garbage_collected`. + pub unsafe fn instance_to_rust_resource( + instance: *mut T, + ) -> *mut ffi::RustResource { + let offset = object_offset_for::(); + unsafe { + instance + .cast::() + .sub(offset) + .cast::() + } + } + + #[derive(Clone, Copy)] + pub struct Ptr { + pointer: NonNull, + _phantom: PhantomData, + } + + impl Ptr { + pub fn get(&self) -> &T { + unsafe { &*get_object_from_rust_resource(self.pointer.as_ptr()) } + } + + pub fn as_rust_resource(&self) -> *mut ffi::RustResource { + self.pointer.as_ptr() + } + } + + impl std::ops::Deref for Ptr { + type Target = T; + + fn deref(&self) -> &T { + self.get() + } + } + + impl std::fmt::Debug for Ptr { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Debug::fmt(&**self, fmt) + } + } + + // cppgc handles cannot be bitwise moved after construction due to internal + // back-pointers. We use Box to ensure stable addresses. + + struct PersistentInner(Box<[usize; PERSISTENT_USIZE_COUNT]>); + + impl PersistentInner { + fn new(resource: *mut ffi::RustResource) -> Self { + let mut storage = Box::new([0usize; PERSISTENT_USIZE_COUNT]); + unsafe { ffi::cppgc_persistent_construct(storage.as_mut_ptr() as usize, resource) } + Self(storage) + } + + fn get(&self) -> *mut ffi::RustResource { + unsafe { ffi::cppgc_persistent_get(self.0.as_ptr() as usize) } + } + } + + impl Drop for PersistentInner { + fn drop(&mut self) { + unsafe { ffi::cppgc_persistent_destruct(self.0.as_mut_ptr() as usize) } + } + } + + pub struct Handle { + inner: UnsafeCell>, + } + + impl Default for Handle { + fn default() -> Self { + Self::new() + } + } + + impl Handle { + pub fn new() -> Self { + Self { + inner: UnsafeCell::new(None), + } + } + + /// # Safety + /// The resource pointer must be valid and allocated via `make_garbage_collected`. + pub unsafe fn from_resource(resource: *mut ffi::RustResource) -> Self { + Self { + inner: UnsafeCell::new(Some(PersistentInner::new(resource))), + } + } + + pub fn has_persistent(&self) -> bool { + // SAFETY: V8 isolate is single-threaded + unsafe { (*self.inner.get()).is_some() } + } + + pub fn get_resource(&self) -> Option> { + // SAFETY: V8 isolate is single-threaded + unsafe { + (*self.inner.get()) + .as_ref() + .and_then(|p| NonNull::new(p.get())) + } + } + + /// Clears the persistent handle, releasing the GC root. + /// This enables cycle collection for traced refs. + pub fn clear(&self) { + // SAFETY: V8 isolate is single-threaded + unsafe { + (*self.inner.get()).take(); + } + } + + pub fn release(&mut self) { + // SAFETY: We have &mut self + unsafe { + (*self.inner.get()).take(); + } + } + } + + struct WeakPersistentInner(Box<[usize; WEAK_PERSISTENT_USIZE_COUNT]>); + + impl WeakPersistentInner { + fn new(resource: *mut ffi::RustResource) -> Self { + let mut storage = Box::new([0usize; WEAK_PERSISTENT_USIZE_COUNT]); + unsafe { ffi::cppgc_weak_persistent_construct(storage.as_mut_ptr() as usize, resource) } + Self(storage) + } + + fn get(&self) -> *mut ffi::RustResource { + unsafe { ffi::cppgc_weak_persistent_get(self.0.as_ptr() as usize) } + } + } + + impl Drop for WeakPersistentInner { + fn drop(&mut self) { + unsafe { ffi::cppgc_weak_persistent_destruct(self.0.as_mut_ptr() as usize) } + } + } + + #[derive(Default)] + pub struct WeakHandle { + inner: Option, + } + + impl WeakHandle { + /// # Safety + /// The resource pointer must be valid and allocated via `make_garbage_collected`. + pub unsafe fn from_resource(resource: *mut ffi::RustResource) -> Self { + Self { + inner: Some(WeakPersistentInner::new(resource)), + } + } + + pub fn get(&self) -> Option> { + self.inner.as_ref().and_then(|p| NonNull::new(p.get())) + } + + pub fn is_alive(&self) -> bool { + self.get().is_some() + } + } + + pub(super) struct MemberInner(Box>); + + impl MemberInner { + fn new(resource: *mut ffi::RustResource) -> Self { + let storage = Box::new(UnsafeCell::new([0usize; MEMBER_USIZE_COUNT])); + unsafe { ffi::cppgc_member_construct(storage.get() as usize, resource) } + Self(storage) + } + + fn get(&self) -> *mut ffi::RustResource { + unsafe { ffi::cppgc_member_get(self.0.get() as usize) } + } + + fn assign(&self, resource: *mut ffi::RustResource) { + unsafe { ffi::cppgc_member_assign(self.0.get() as usize, resource) } + } + + pub(super) fn as_usize(&self) -> usize { + self.0.get() as usize + } + } + + impl Drop for MemberInner { + fn drop(&mut self) { + unsafe { ffi::cppgc_member_destruct(self.0.get() as usize) } + } + } + + pub struct Member { + pub(super) inner: UnsafeCell>, + } + + impl Default for Member { + fn default() -> Self { + Self::new() + } + } + + impl Member { + pub fn new() -> Self { + Self { + inner: UnsafeCell::new(None), + } + } + + pub fn from_resource(resource: NonNull) -> Self { + Self { + inner: UnsafeCell::new(Some(MemberInner::new(resource.as_ptr()))), + } + } + + pub fn get(&self) -> Option> { + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + unsafe { + (*self.inner.get()) + .as_ref() + .and_then(|m| NonNull::new(m.get())) + } + } + + pub fn set(&self, resource: Option>) { + let resource_ptr = resource.map_or(std::ptr::null_mut(), NonNull::as_ptr); + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + unsafe { + if let Some(ref m) = *self.inner.get() { + m.assign(resource_ptr); + } else if let Some(r) = resource { + *self.inner.get() = Some(MemberInner::new(r.as_ptr())); + } + } + } + } + + pub(super) struct WeakMemberInner(Box>); + + impl WeakMemberInner { + fn new(resource: *mut ffi::RustResource) -> Self { + let storage = Box::new(UnsafeCell::new([0usize; WEAK_MEMBER_USIZE_COUNT])); + unsafe { ffi::cppgc_weak_member_construct(storage.get() as usize, resource) } + Self(storage) + } + + fn get(&self) -> *mut ffi::RustResource { + unsafe { ffi::cppgc_weak_member_get(self.0.get() as usize) } + } + + fn assign(&self, resource: *mut ffi::RustResource) { + unsafe { ffi::cppgc_weak_member_assign(self.0.get() as usize, resource) } + } + + pub(super) fn as_usize(&self) -> usize { + self.0.get() as usize + } + } + + impl Drop for WeakMemberInner { + fn drop(&mut self) { + unsafe { ffi::cppgc_weak_member_destruct(self.0.get() as usize) } + } + } + + pub struct WeakMember { + pub(super) inner: UnsafeCell>, + } + + impl Default for WeakMember { + fn default() -> Self { + Self::new() + } + } + + impl WeakMember { + pub fn new() -> Self { + Self { + inner: UnsafeCell::new(None), + } + } + + pub fn from_resource(resource: NonNull) -> Self { + Self { + inner: UnsafeCell::new(Some(WeakMemberInner::new(resource.as_ptr()))), + } + } + + pub fn get(&self) -> Option> { + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + unsafe { + (*self.inner.get()) + .as_ref() + .and_then(|m| NonNull::new(m.get())) + } + } + + pub fn is_alive(&self) -> bool { + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + unsafe { (*self.inner.get()).is_some() } + } + + pub fn set(&self, resource: Option>) { + let resource_ptr = resource.map_or(std::ptr::null_mut(), NonNull::as_ptr); + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + unsafe { + if let Some(ref m) = *self.inner.get() { + m.assign(resource_ptr); + } else if let Some(r) = resource { + *self.inner.get() = Some(WeakMemberInner::new(r.as_ptr())); + } + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn handle_default_is_empty() { + let handle = Handle::new(); + assert!(!handle.has_persistent()); + assert!(handle.get_resource().is_none()); + } + + #[test] + fn member_default_is_empty() { + let member = Member::new(); + assert!(member.get().is_none()); + } + + #[test] + fn weak_member_default_is_empty() { + let weak_member = WeakMember::new(); + assert!(!weak_member.is_alive()); + assert!(weak_member.get().is_none()); + } + } +} + +pub trait GarbageCollected { + fn trace(&self, _visitor: &mut GcVisitor<'_>) {} + + fn get_name(&self) -> Option<&'static std::ffi::CStr> { + None + } +} + +/// Trait for accessing parent context during GC visitation. +/// +/// This is implemented by `Instance` to provide context for dynamic +/// strong/traced switching of `Ref`s. +pub trait GcParent { + /// Returns the current strong reference count. + fn strong_refcount(&self) -> u32; + + /// Returns whether this instance has a JS wrapper. + fn has_wrapper(&self) -> bool; +} + +/// Visitor for garbage collection tracing. +/// +/// `GcVisitor` wraps the cppgc visitor and tracks parent context for dynamic +/// strong/traced switching of `Ref`s. +pub struct GcVisitor<'a> { + visitor: ffi::CppgcVisitor, + /// Reference to the parent being traced (used for dynamic Ref switching) + parent: Option<&'a dyn GcParent>, +} + +impl GcVisitor<'_> { + /// Creates a new `GcVisitor` from a raw cppgc visitor. + /// + /// # Safety + /// The visitor must be valid for the lifetime of this `GcVisitor`. + pub unsafe fn from_raw(visitor: ffi::CppgcVisitor) -> Self { + Self { + visitor, + parent: None, + } + } + + /// Creates a child visitor with the given parent context. + /// + /// Use this when tracing through a resource to provide context for + /// dynamic strong/traced switching of child `Ref`s. + pub fn with_parent<'b>(&mut self, parent: &'b dyn GcParent) -> GcVisitor<'b> { + GcVisitor { + visitor: ffi::CppgcVisitor { + ptr: self.visitor.ptr, + }, + parent: Some(parent), + } + } + + /// Returns the parent context if available. + pub fn parent(&self) -> Option<&dyn GcParent> { + self.parent + } + + pub fn trace(&mut self, handle: &TracedReference) { + unsafe { + ffi::cppgc_visitor_trace(std::ptr::from_mut(&mut self.visitor), handle.as_ffi_ref()); + } + } + + pub fn trace_member(&mut self, member: &cppgc::Member) { + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + // We're in a GC trace callback, so the member is guaranteed to be valid. + if let Some(m) = unsafe { &*member.inner.get() } { + unsafe { + ffi::cppgc_visitor_trace_member( + std::ptr::from_mut(&mut self.visitor), + m.as_usize(), + ); + } + } + } + + pub fn trace_weak_member(&mut self, member: &cppgc::WeakMember) { + // SAFETY: V8 isolate is single-threaded, so no concurrent access is possible. + // We're in a GC trace callback, so the member is guaranteed to be valid. + if let Some(m) = unsafe { &*member.inner.get() } { + unsafe { + ffi::cppgc_visitor_trace_weak_member( + std::ptr::from_mut(&mut self.visitor), + m.as_usize(), + ); + } + } + } + + /// Visits a `Ref` during GC tracing, handling dynamic strong/traced switching. + /// + /// This method: + /// 1. Calls `ref.visit()` to potentially switch the ref between strong and traced, + /// and trace the Member storage if in traced mode + /// 2. Traces the target's wrapper if it exists + /// 3. If no wrapper, recursively traces through the target's children + /// + /// This mirrors the behavior of `Wrappable::visitRef` in C++. + pub fn visit_ref(&mut self, r: &crate::Ref) { + let instance = r.visit(self); + + if let Some(wrapper) = instance.traced_reference() { + // Target has a wrapper - trace it + self.trace(wrapper); + } else { + // No wrapper - trace transitively through the target's children + let mut child_visitor = self.with_parent(instance); + instance.resource.trace(&mut child_visitor); + } + } +} + /// A safe wrapper around a V8 isolate pointer. /// -/// `Isolate` provides a type-safe abstraction over raw `v8::Isolate*` pointers, +/// `IsolatePtr` provides a type-safe abstraction over raw `v8::Isolate*` pointers, /// ensuring that the pointer is always non-null. This type is `Copy` and can be /// freely passed around without worrying about ownership. /// /// # Thread Safety /// -/// V8 isolates are single-threaded. While `Isolate` itself is `Send` and `Sync` +/// V8 isolates are single-threaded. While `IsolatePtr` itself is `Send` and `Sync` /// (as it's just a pointer wrapper), V8 operations must only be performed on the /// thread that owns the isolate lock. Use `is_locked()` to verify the current /// thread holds the lock before performing V8 operations. @@ -1227,7 +1997,7 @@ impl<'a> FunctionCallbackInfo<'a> { /// /// ```ignore /// // Create from raw pointer (unsafe) -/// let isolate = unsafe { v8::Isolate::from_ffi(raw_ptr) }; +/// let isolate = unsafe { v8::IsolatePtr::from_ffi(raw_ptr) }; /// /// // Check if locked before V8 operations /// assert!(unsafe { isolate.is_locked() }); @@ -1241,7 +2011,7 @@ pub struct IsolatePtr { } impl IsolatePtr { - /// Creates an `Isolate` from a raw pointer. + /// Creates an `IsolatePtr` from a raw pointer. /// /// # Safety /// The pointer must be non-null and point to a valid V8 isolate. @@ -1252,6 +2022,12 @@ impl IsolatePtr { } } + /// Creates an `IsolatePtr` from a `NonNull` pointer. + pub fn from_non_null(handle: NonNull) -> Self { + debug_assert!(unsafe { ffi::isolate_is_locked(handle.as_ptr()) }); + Self { handle } + } + /// Returns whether this isolate is currently locked by the current thread. /// /// # Safety @@ -1265,4 +2041,9 @@ impl IsolatePtr { pub fn as_ffi(&self) -> *mut ffi::Isolate { self.handle.as_ptr() } + + /// Returns the `NonNull` pointer to the V8 isolate. + pub fn as_non_null(&self) -> NonNull { + self.handle + } }