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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/engine/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ mod datatypes;
mod jsobject;
mod operations;
mod property_map;
mod weak;

pub mod shape;

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");
Expand Down
88 changes: 88 additions & 0 deletions core/engine/src/object/weak.rs
Original file line number Diff line number Diff line change
@@ -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<JsObject<T>>` 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<T: NativeObject = ErasedObjectData> {
inner: WeakGc<VTableObject<T>>,
}

impl<T: NativeObject> WeakJsObject<T> {
/// Creates a new weak reference to the given [`JsObject`].
#[inline]
#[must_use]
pub fn new(object: &JsObject<T>) -> 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<JsObject<T>> {
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<T: NativeObject> From<&JsObject<T>> for WeakJsObject<T> {
fn from(object: &JsObject<T>) -> Self {
Self::new(object)
}
}

impl<T: NativeObject> Clone for WeakJsObject<T> {
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<T: NativeObject> Debug for WeakJsObject<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("WeakJsObject")
.field(&self.upgrade())
.finish()
}
}
60 changes: 60 additions & 0 deletions examples/src/bin/weak_js_object.rs
Original file line number Diff line number Diff line change
@@ -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<u32, WeakJsObject> = 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()
);
}