diff --git a/core/engine/src/object/mod.rs b/core/engine/src/object/mod.rs index 258da691647..19e470928f9 100644 --- a/core/engine/src/object/mod.rs +++ b/core/engine/src/object/mod.rs @@ -36,6 +36,7 @@ mod datatypes; mod jsobject; mod operations; mod property_map; +mod weak; pub mod shape; @@ -43,6 +44,7 @@ pub(crate) use builtins::*; pub use datatypes::JsData; pub use jsobject::*; +pub use weak::WeakJsObject; /// Const `constructor`, usually set on prototypes as a key to point to their respective constructor object. pub const CONSTRUCTOR: JsString = js_string!("constructor"); diff --git a/core/engine/src/object/weak.rs b/core/engine/src/object/weak.rs new file mode 100644 index 00000000000..e7a3e0da747 --- /dev/null +++ b/core/engine/src/object/weak.rs @@ -0,0 +1,88 @@ +//! This module implements the [`WeakJsObject`] structure. +//! +//! A [`WeakJsObject`] is a weak reference to a [`JsObject`], allowing an embedder to hold a +//! reference to an object without keeping it alive across garbage collections. + +use super::{ErasedObjectData, JsObject, NativeObject, jsobject::VTableObject}; +use boa_gc::{Finalize, Trace, WeakGc}; +use std::fmt::{self, Debug}; + +/// A weak reference to a [`JsObject`]. +/// +/// This is the object-level counterpart of [`boa_gc::WeakGc`]. It lets embedders keep a handle to a +/// [`JsObject`] without preventing it from being collected. Because the referenced object may be +/// collected at any point, [`WeakJsObject::upgrade`] returns an `Option>` that is `None` +/// once the object is gone. +/// +/// # Examples +/// +/// ``` +/// # use boa_engine::object::{JsObject, WeakJsObject}; +/// let object = JsObject::with_null_proto(); +/// let weak = WeakJsObject::new(&object); +/// +/// // While `object` is alive, the weak reference can be upgraded. +/// assert!(weak.upgrade().is_some()); +/// ``` +#[derive(Trace, Finalize)] +pub struct WeakJsObject { + inner: WeakGc>, +} + +impl WeakJsObject { + /// Creates a new weak reference to the given [`JsObject`]. + #[inline] + #[must_use] + pub fn new(object: &JsObject) -> Self { + Self { + inner: WeakGc::new(object.inner()), + } + } + + /// Upgrades the weak reference to a strong [`JsObject`] if the referenced object is still live, + /// or returns `None` if it was already garbage collected. + #[inline] + #[must_use] + pub fn upgrade(&self) -> Option> { + self.inner.upgrade().map(JsObject::from_inner) + } + + /// Checks whether this weak reference can still be upgraded to a live [`JsObject`]. + #[inline] + #[must_use] + pub fn is_upgradable(&self) -> bool { + self.inner.is_upgradable() + } +} + +impl From<&JsObject> for WeakJsObject { + fn from(object: &JsObject) -> Self { + Self::new(object) + } +} + +impl Clone for WeakJsObject { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +// `PartialEq`/`Eq`/`Hash` are intentionally not implemented. The natural forwarding to `WeakGc` +// would compare and hash by the *live* referent, so two references to the same object would stop +// comparing equal (and change their hash) once that object is collected, breaking `Eq`'s +// reflexivity and the `Hash`/`Eq` invariant for a collected key. To compare two weak references, +// upgrade them first and compare the resulting `JsObject`s. These impls can be added later, without +// a breaking change, if a collection-stable identity is designed. + +// We can't derive `Debug` because `VTableObject` deliberately doesn't implement it. Instead we +// upgrade and delegate to `JsObject`'s own `Debug`, which uses a `RecursionLimiter` to avoid +// overflowing the stack on cyclic object graphs. A collected referent prints as `None`. +impl Debug for WeakJsObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("WeakJsObject") + .field(&self.upgrade()) + .finish() + } +} diff --git a/examples/src/bin/weak_js_object.rs b/examples/src/bin/weak_js_object.rs new file mode 100644 index 00000000000..a42e5d540d4 --- /dev/null +++ b/examples/src/bin/weak_js_object.rs @@ -0,0 +1,60 @@ +//! Example demonstrating the `WeakJsObject` API: an embedder-held weak reference to a `JsObject` +//! that does not keep the referenced object alive across garbage collections. +use boa_engine::{Context, Source, js_string, object::WeakJsObject}; +use boa_gc::force_collect; +use std::collections::HashMap; + +fn main() { + let mut context = Context::default(); + + // Create two independent JS objects. For now we hold strong handles to both. + let first = context + .eval(Source::from_bytes("({ name: 'first' })")) + .unwrap() + .as_object() + .unwrap() + .clone(); + let second = context + .eval(Source::from_bytes("({ name: 'second' })")) + .unwrap() + .as_object() + .unwrap() + .clone(); + + // A Rust-side registry that remembers objects by an embedder-chosen id *without* keeping them + // alive. This is the shape of use case `WeakJsObject` exists for: for example, mapping host DOM + // nodes to their JS wrappers so that the same node keeps yielding the same object, while still + // letting the engine collect a wrapper once nothing else references it. + let mut registry: HashMap = HashMap::new(); + registry.insert(1, WeakJsObject::new(&first)); + registry.insert(2, WeakJsObject::new(&second)); + + // While the strong handles live, the registry can hand the objects back. + println!("id 1 upgradable? {}", registry[&1].is_upgradable()); + println!("id 2 upgradable? {}", registry[&2].is_upgradable()); + println!("debug of id 1: {:?}", registry[&1]); + + // Drop the strong handle to the first object and run a collection. Its registry entry can no + // longer be upgraded, and the weak entry never kept the object alive in the first place. + drop(first); + force_collect(); + + println!("after dropping `first` and collecting:"); + println!(" id 1 upgradable? {}", registry[&1].is_upgradable()); + println!(" id 2 upgradable? {}", registry[&2].is_upgradable()); + + // The surviving object still round-trips through its weak reference. Upgrading yields a strong + // handle, which keeps the object alive for as long as it is held. + let recovered = registry[&2].upgrade().expect("`second` is still alive"); + let name = recovered.get(js_string!("name"), &mut context).unwrap(); + println!(" id 2 name = {}", name.display()); + + // Drop the last strong handles (the original and the upgraded one); now nothing is upgradable. + drop(second); + drop(recovered); + force_collect(); + println!( + "after dropping everything: id 2 upgradable? {}", + registry[&2].is_upgradable() + ); +}