Skip to content
File

Blob: src/rust/jsg/resource.rs

rust488 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5use std::any::TypeId;
6use std::cell::Cell;
7use std::collections::HashMap;
8use std::fmt;
9use std::ops::Deref;
10use std::ptr::NonNull;
11 
12use kj_rs::KjMaybe;
13 
14use crate::ConstantValue;
15use crate::Error;
16use crate::FromJS;
17use crate::GarbageCollected;
18use crate::Lock;
19use crate::Member;
20use crate::ToJS;
21use crate::Traced;
22use crate::Type;
23use crate::v8;
24use crate::v8::ffi::Wrappable;
25 
26/// A reference-counted smart pointer to a Rust resource that integrates with
27/// V8's garbage collector.
28///
29/// Named `Rc` to mirror both [`std::rc::Rc`] (reference-counted shared ownership)
30/// and C++ `jsg::Ref` (the GC-integrated reference type in workerd's C++ JSG layer).
31/// Like `std::rc::Rc`, cloning is cheap (increments a refcount). Unlike `std::rc::Rc`,
32/// the destructor also notifies the GC so JavaScript wrappers can be collected.
33///
34/// Each `Rc<R>` holds:
35/// - An `std::rc::Rc<R>` for Rust-side shared ownership (enables `Weak`)
36/// - A [`v8::WrappableRc`] for `kj::Rc` refcounting and GC integration
37///
38/// **GC integration:**
39/// - On `Clone`, calls `wrappable_add_strong_ref` (C++ `addStrongRef`).
40/// - On `Drop`, calls `wrappable_remove_strong_ref` with the ref's current
41/// `strong` flag (C++ `maybeDeferDestruction`), then fields drop in
42/// declaration order.
43pub struct Rc<R: Resource> {
44 /// Shared ownership of the Rust resource.
45 handle: std::rc::Rc<R>,
46 /// Owned, reference-counted handle to the C++ Wrappable.
47 wrappable: v8::WrappableRc,
48 /// Per-Rc GC state: pointer to the parent `Wrappable`.
49 /// `None` until first visited. Set by `visitRef` during GC tracing.
50 parent: Cell<Option<NonNull<Wrappable>>>,
51 /// Per-Rc GC state: whether this ref is currently strong.
52 /// Starts `true` (addStrongRef was called). Toggled by `visitRef` during GC tracing.
53 strong: Cell<bool>,
54}
55 
56impl<R: Resource> Rc<R> {
57 /// Creates a new `Rc<R>` that ties a Rust resource's lifetime to the
58 /// JavaScript virtual machine's garbage collector.
59 ///
60 /// This is the primary way to create a Rust resource that can be exposed
61 /// to JavaScript. The returned `Rc` can then be converted to a JS object
62 /// via [`ToJS::to_js`](crate::ToJS::to_js).
63 ///
64 /// The resource stays alive as long as at least one `Rc` exists **or** a
65 /// JavaScript wrapper object is reachable from the JS heap. When all Rust
66 /// `Rc`s are dropped and the JS wrapper becomes unreachable, V8's garbage
67 /// collector will destroy the resource.
68 pub fn new(resource: R) -> Self {
69 let handle = std::rc::Rc::new(resource);
70 
71 // Leak an Rc clone as a fat pointer to dyn GarbageCollected and hand
72 // ownership to a new Wrappable on the KJ heap. Since R: GarbageCollected,
73 // the vtable dispatches trace/get_name directly to R's implementations.
74 let raw: *const R = std::rc::Rc::into_raw(std::rc::Rc::clone(&handle));
75 let fat: *mut dyn GarbageCollected = raw as *const dyn GarbageCollected as *mut _;
76 let trait_object = v8::ffi::TraitObjectPtr::from_raw(fat, std::any::TypeId::of::<R>());
77 let wrappable: v8::WrappableRc = trait_object.into();
78 
79 Self {
80 handle,
81 wrappable,
82 parent: Cell::new(None),
83 strong: Cell::new(true),
84 }
85 }
86 
87 /// Visits this Rc during GC tracing.
88 ///
89 /// Delegates to C++ `Wrappable::visitRef()` which handles strong/traced switching
90 /// and transitive tracing.
91 ///
92 /// Takes `&self` because `Traced::trace(&self)` receives a shared
93 /// reference to `R` inside the `Rc` allocation. The `parent` and `strong`
94 /// fields already use `Cell` for interior mutability.
95 /// `WrappableRc::visit_rc(&self)` produces `Pin<&mut Wrappable>` from
96 /// the raw pointer inside `KjRc` — the mutation target is the C++ heap
97 /// object, not the `WrappableRc` wrapper itself.
98 pub(crate) fn visit(&self, visitor: &mut v8::GcVisitor) {
99 self.wrappable.visit_rc(
100 self.parent.as_ptr().cast::<usize>(),
101 self.strong.as_ptr(),
102 visitor,
103 );
104 }
105 
106 /// Creates a [`Weak`] reference to this resource.
107 ///
108 /// The weak reference does not prevent GC collection. Use
109 /// [`Weak::upgrade`] to obtain a strong `Rc<R>` if the resource is
110 /// still alive.
111 ///
112 /// Mirrors [`std::rc::Rc::downgrade`](std::rc::std::rc::Rc::downgrade).
113 pub fn downgrade(&self) -> Weak<R> {
114 Weak::from(self)
115 }
116 
117 /// Attaches this resource to the `this` object in a V8 constructor callback.
118 ///
119 /// Called from `#[jsg_constructor]`-generated code. V8 has already created
120 /// the `this` object from the `FunctionTemplate`'s `InstanceTemplate`;
121 /// this method attaches the `Wrappable` to it so that instance methods
122 /// can resolve the resource via `resolve_resource`.
123 pub fn attach_to_this(&self, info: &mut v8::FunctionCallbackInfo) {
124 self.wrappable.attach_to_this(info);
125 }
126 
127 /// Returns the C++ Wrappable's strong reference count.
128 #[cfg(debug_assertions)]
129 pub fn strong_refcount(&self) -> u32 {
130 self.wrappable.strong_refcount()
131 }
132}
133 
134impl<R: Resource> Deref for Rc<R> {
135 type Target = R;
136 
137 #[inline]
138 fn deref(&self) -> &Self::Target {
139 &self.handle
140 }
141}
142 
143impl<R: Resource> Clone for Rc<R> {
144 fn clone(&self) -> Self {
145 let mut wrappable = self.wrappable.clone();
146 wrappable.add_strong_ref();
147 Self {
148 handle: std::rc::Rc::clone(&self.handle),
149 wrappable,
150 parent: Cell::new(None),
151 strong: Cell::new(true),
152 }
153 }
154}
155 
156impl<R: Resource> Drop for Rc<R> {
157 fn drop(&mut self) {
158 self.wrappable.remove_strong_ref(self.strong.get());
159 }
160}
161 
162impl<R: Resource + 'static> ToJS for Rc<R> {
163 fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value>
164 where
165 'b: 'a,
166 {
167 wrap(lock, self)
168 }
169}
170 
171impl<R: Resource + 'static> FromJS for Rc<R> {
172 type ResultType = Self;
173 
174 fn from_js(lock: &mut Lock, value: v8::Local<v8::Value>) -> Result<Self, Error> {
175 // Capture the JS type name before consuming the value, for error messages.
176 let type_name = value.type_of();
177 
178 let mut wrappable = v8::WrappableRc::from_js(lock.isolate(), value).ok_or_else(|| {
179 Error::new_type_error(format!("expected {}, got {type_name}", R::class_name()))
180 })?;
181 
182 let resource_ptr = wrappable.resolve_resource::<R>().ok_or_else(|| {
183 Error::new_type_error(format!("expected {}, got {type_name}", R::class_name()))
184 })?;
185 
186 // SAFETY: The pointer came from std::rc::Rc::into_raw in Rc::new(), and the
187 // Wrappable being alive guarantees the Rc allocation is still valid.
188 // increment_strong_count bumps the count so from_raw can safely create
189 // a second Rc handle without double-freeing.
190 let handle = unsafe {
191 std::rc::Rc::increment_strong_count(resource_ptr.as_ptr());
192 std::rc::Rc::from_raw(resource_ptr.as_ptr())
193 };
194 
195 wrappable.add_strong_ref();
196 Ok(Self {
197 handle,
198 wrappable,
199 parent: Cell::new(None),
200 strong: Cell::new(true),
201 })
202 }
203}
204 
205/// Equality by pointer identity — two `Rc`s are equal iff they point to the
206/// same allocation, matching the behaviour of [`std::rc::Rc`].
207impl<R: Resource> PartialEq for Rc<R> {
208 fn eq(&self, other: &Self) -> bool {
209 std::rc::Rc::ptr_eq(&self.handle, &other.handle)
210 }
211}
212 
213impl<R: Resource> Eq for Rc<R> {}
214 
215/// Ordering by pointer address — consistent with `PartialEq` and allows
216/// `Rc<T>` to be used as a `BTreeSet`/`BTreeMap` key.
217impl<R: Resource> PartialOrd for Rc<R> {
218 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
219 Some(self.cmp(other))
220 }
221}
222 
223impl<R: Resource> Ord for Rc<R> {
224 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
225 std::rc::Rc::as_ptr(&self.handle).cmp(&std::rc::Rc::as_ptr(&other.handle))
226 }
227}
228 
229/// Hashing by pointer address — consistent with `PartialEq` and allows
230/// `Rc<T>` to be used as a `HashSet`/`HashMap` key.
231impl<R: Resource> std::hash::Hash for Rc<R> {
232 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
233 std::rc::Rc::as_ptr(&self.handle).hash(state);
234 }
235}
236 
237impl<R: Resource> fmt::Debug for Rc<R> {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 f.debug_struct("Rc")
240 .field("type", &std::any::type_name::<R>())
241 .field("rc_strong", &std::rc::Rc::strong_count(&self.handle))
242 .finish_non_exhaustive()
243 }
244}
245 
246/// A weak reference to a resource that doesn't prevent resource destruction.
247///
248/// Uses `Weak<R>` for liveness detection: when all `Rc`s drop and
249/// `~Wrappable` drops the `Rc` (via `std::rc::Rc::from_raw`), the `std::rc::Rc<R>` reaches
250/// 0 and all `Weak`s expire.
251///
252/// Does NOT hold a `WrappableRc` — does not keep the `kj::std::rc::Rc<Wrappable>` alive.
253/// Stores a non-owning pointer to the Wrappable so that `upgrade()` can
254/// reconstruct a `WrappableRc` via `wrappable_to_rc`. The pointer is only
255/// dereferenced after `Weak::upgrade` confirms the resource (and thus the
256/// Wrappable) is still alive.
257pub struct Weak<R: Resource> {
258 weak: std::rc::Weak<R>,
259 /// Non-owning pointer to the C++ Wrappable. `None` for default-constructed
260 /// `Weak`s (which can never be upgraded). Valid as long as the resource
261 /// is alive (verified by `Weak::upgrade` before use).
262 wrappable: Option<NonNull<Wrappable>>,
263}
264 
265impl<R: Resource> Weak<R> {
266 /// Returns `true` if the resource is still alive.
267 #[inline]
268 pub fn is_alive(&self) -> bool {
269 self.weak.strong_count() > 0
270 }
271 
272 /// Upgrades to a strong `Rc<R>` if the resource is still alive.
273 pub fn upgrade(&self) -> Option<Rc<R>> {
274 let handle = self.weak.upgrade()?;
275 let wrappable_ptr = self.wrappable?;
276 // Reconstruct a WrappableRc from the stored wrappable pointer.
277 // SAFETY: The resource is alive (Weak::upgrade succeeded), so the Wrappable
278 // is alive too (the Wrappable's Rc clone keeps the resource alive, which
279 // means the Wrappable must still exist — its destruction via std::rc::Rc::from_raw
280 // would have released the Rc and thus the resource).
281 let mut wrappable = unsafe { v8::WrappableRc::from_raw_wrappable(wrappable_ptr.as_ptr()) };
282 wrappable.add_strong_ref();
283 Some(Rc {
284 handle,
285 wrappable,
286 parent: Cell::new(None),
287 strong: Cell::new(true),
288 })
289 }
290}
291 
292impl<R: Resource> From<&Rc<R>> for Weak<R> {
293 fn from(r: &Rc<R>) -> Self {
294 Self {
295 weak: std::rc::Rc::downgrade(&r.handle),
296 wrappable: Some(r.wrappable.as_ptr()),
297 }
298 }
299}
300 
301impl<R: Resource> Clone for Weak<R> {
302 fn clone(&self) -> Self {
303 Self {
304 weak: std::rc::Weak::clone(&self.weak),
305 wrappable: self.wrappable,
306 }
307 }
308}
309 
310/// `Rc<R>` is a strong GC edge — visited via `GcVisitor::visit_rc`.
311impl<R: Resource> Traced for Rc<R> {
312 fn trace(&self, visitor: &mut v8::GcVisitor) {
313 visitor.visit_rc(self);
314 }
315}
316 
317/// Weak references don't keep the target alive and have no GC edges to trace.
318impl<R: Resource> Traced for Weak<R> {}
319 
320impl<R: Resource> GarbageCollected for Weak<R> {
321 fn memory_name(&self) -> &'static std::ffi::CStr {
322 // jsgGetMemoryName is only called on live Wrappables, never on Weak<R>.
323 // Delegate to the concrete R via a live upgrade. In the (unreachable)
324 // case where the referent is already gone, return an empty string.
325 self.weak.upgrade().as_deref().map_or(c"", R::memory_name)
326 }
327}
328 
329impl<R: Resource> Default for Weak<R> {
330 fn default() -> Self {
331 Self {
332 weak: std::rc::Weak::new(),
333 wrappable: None,
334 }
335 }
336}
337 
338/// Wraps a `Rc<R>` as a JavaScript object backed by the resource's `FunctionTemplate`.
339///
340/// **Identity**: Multiple calls with `Rc`s that share the same underlying `Wrappable`
341/// (i.e. clones of the same `Rc`) return the **same** JS object — V8 caches the
342/// wrapper on the `Wrappable` via `CppgcShim`. This means:
343///
344/// ```text
345/// let a = resource.clone().to_js(lock);
346/// let b = resource.to_js(lock);
347/// // a === b in JavaScript (same object identity)
348/// ```
349///
350/// **Prototype**: The returned object's prototype is set up from the resource's
351/// [`FunctionTemplate`], which includes all `#[jsg_method]` instance methods and
352/// `#[jsg_static_constant]` values registered via [`Resource::members()`].
353///
354/// **Ownership**: The `Rc` is consumed. The JS wrapper keeps the `Wrappable`
355/// alive via `CppgcShim`. When the JS object is garbage collected and all Rust
356/// `Rc`s are dropped, the resource is destroyed.
357#[expect(clippy::needless_pass_by_value)]
358pub fn wrap<'a, R: Resource + 'static>(
359 lock: &mut Lock,
360 resource: Rc<R>,
361) -> v8::Local<'a, v8::Value> {
362 let isolate = lock.isolate();
363 let constructor = lock.realm().resources.get_constructor::<R>(isolate);
364 
365 resource.wrappable.to_js(isolate, constructor)
366}
367 
368/// Returns the constructor function for a resource type as a `Local<Function>`.
369///
370/// Lazily creates and caches the `FunctionTemplate` in the `Realm` on first call.
371/// The returned function can be exposed as a global (e.g., `ctx.set_global("MyClass", func)`).
372pub fn function_template_of<'a, R: Resource + 'static>(
373 lock: &mut Lock,
374) -> v8::Local<'a, v8::Function> {
375 let isolate = lock.isolate();
376 let constructor = lock.realm().resources.get_constructor::<R>(isolate);
377 // SAFETY: `isolate` is valid and locked (Lock invariant). `constructor` is a valid
378 // Global<FunctionTemplate> cached in the Realm. Inlined from `as_local_function` to
379 // avoid re-borrowing `self` while `constructor` holds an immutable borrow through `realm()`.
380 unsafe {
381 v8::Local::from_ffi(
382 isolate,
383 v8::ffi::function_template_get_function(isolate.as_ffi(), constructor.as_ffi_ref()),
384 )
385 }
386}
387 
388/// Builds a [`ResourceDescriptor`] from `R`'s [`Resource::members()`] list.
389/// The descriptor is passed to C++ `create_resource_template` to set up the
390/// V8 `FunctionTemplate` with the correct methods, constants, and constructor.
391fn get_resource_descriptor<R: Resource>() -> v8::ffi::ResourceDescriptor {
392 let mut descriptor = v8::ffi::ResourceDescriptor {
393 name: R::class_name().to_owned(),
394 constructor: KjMaybe::None,
395 methods: Vec::new(),
396 properties: Vec::new(),
397 static_methods: Vec::new(),
398 static_constants: Vec::new(),
399 };
400 
401 for m in R::members() {
402 match m {
403 Member::Constructor { callback } => {
404 descriptor.constructor = KjMaybe::Some(v8::ffi::ConstructorDescriptor {
405 callback: callback as usize,
406 });
407 }
408 Member::Method { name, callback } => {
409 descriptor.methods.push(v8::ffi::MethodDescriptor {
410 name,
411 callback: callback as usize,
412 });
413 }
414 Member::Property {
415 name,
416 kind,
417 getter_callback,
418 setter_callback,
419 } => {
420 descriptor.properties.push(v8::ffi::PropertyDescriptor {
421 name,
422 kind,
423 getter_callback: getter_callback as usize,
424 setter_callback: setter_callback.map(|f| f as usize).into(),
425 });
426 }
427 Member::StaticMethod { name, callback } => {
428 descriptor.static_methods.push(v8::ffi::MethodDescriptor {
429 name,
430 callback: callback as usize,
431 });
432 }
433 Member::StaticConstant { name, value } => {
434 let ConstantValue::Number(number_value) = value;
435 descriptor
436 .static_constants
437 .push(v8::ffi::StaticConstantDescriptor {
438 name,
439 value: number_value,
440 });
441 }
442 }
443 }
444 
445 descriptor
446}
447 
448#[derive(Default)]
449pub struct Resources {
450 /// Cached V8 `FunctionTemplate`s keyed by resource `TypeId`.
451 templates: HashMap<TypeId, v8::Global<v8::FunctionTemplate>>,
452}
453 
454impl Resources {
455 /// Gets or creates the cached `FunctionTemplate` for a resource type.
456 pub fn get_constructor<R: Resource + 'static>(
457 &mut self,
458 isolate: v8::IsolatePtr,
459 ) -> &v8::Global<v8::FunctionTemplate> {
460 self.templates
461 .entry(TypeId::of::<R>())
462 .or_insert_with(|| Self::create_resource_constructor::<R>(isolate))
463 }
464 
465 /// Creates a new V8 `FunctionTemplate` for resource type `R` via the C++ FFI.
466 fn create_resource_constructor<R: Resource>(
467 isolate: v8::IsolatePtr,
468 ) -> v8::Global<v8::FunctionTemplate> {
469 // SAFETY: Caller guarantees the isolate is valid and locked.
470 unsafe {
471 v8::ffi::create_resource_template(isolate.as_ffi(), &get_resource_descriptor::<R>())
472 .into()
473 }
474 }
475}
476 
477/// Rust types exposed to JavaScript as resource types.
478///
479/// Resource types are passed by reference and call back into Rust when JavaScript accesses
480/// their members. This is analogous to `JSG_RESOURCE_TYPE` in C++ JSG. Resources must provide
481/// member declarations, a cleanup function for GC, and access to their V8 wrapper state.
482pub trait Resource: Type + GarbageCollected + Sized + 'static {
483 /// Returns the list of methods, properties, and constructors exposed to JavaScript.
484 fn members() -> Vec<Member>
485 where
486 Self: Sized;
487}