Skip to content

Commit

Permalink
[[HostDefined]] Improvements (#3460)
Browse files Browse the repository at this point in the history
* Refactor the `[[HostDefined]]` implementation.

Currently `[[HostDefined]]` doesn't permit you to mutably borrow two objects from the `[[HostDefined]]` field since the `FxHashMap` is wrapped under a `GcRefCell`.

This commit implements a `get_mut_many` (from `hashbrown`'s `HashMap`) method to permit accessing several `NativeObject`s using a `NativeTuple`.

Additionally, this commit takes the opportunity to provide automatic downcasting on the `insert` and `remove` methods for `[[HostDefined]]`.

* Update `[[HostDefined]]` example.
  • Loading branch information
johnyob authored Nov 10, 2023
1 parent baa4f98 commit 89fd3be
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 85 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions boa_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ writeable = { workspace = true, optional = true }
yoke = { workspace = true, optional = true }
zerofrom = { workspace = true, optional = true }
fixed_decimal = { workspace = true, features = ["ryu"], optional = true}
hashbrown.workspace = true

[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]
web-time = { version = "0.2.3", optional = true }
Expand Down
173 changes: 108 additions & 65 deletions boa_engine/src/host_defined.rs
Original file line number Diff line number Diff line change
@@ -1,111 +1,154 @@
use std::any::TypeId;

use boa_gc::{GcRef, GcRefCell, GcRefMut};
use boa_macros::{Finalize, Trace};
use rustc_hash::FxHashMap;
use hashbrown::hash_map::HashMap;

use crate::object::NativeObject;

/// Map used to store the host defined objects.
#[doc(hidden)]
type HostDefinedMap = FxHashMap<TypeId, Box<dyn NativeObject>>;

/// This represents a `ECMASCript` specification \[`HostDefined`\] field.
///
/// This allows storing types which are mapped by their [`TypeId`].
#[derive(Default, Trace, Finalize)]
#[allow(missing_debug_implementations)]
pub struct HostDefined {
state: GcRefCell<HostDefinedMap>,
// INVARIANT: All key-value pairs `(id, obj)` satisfy:
// `id == TypeId::of::<T>() && obj.is::<T>()`
// for some type `T : NativeObject`.
types: HashMap<TypeId, Box<dyn NativeObject>>,
}

// TODO: Track https://github.com/rust-lang/rust/issues/65991 and
// https://github.com/rust-lang/rust/issues/90850 to remove this
// when those are stabilized.
fn downcast_boxed_native_object_unchecked<T: NativeObject>(obj: Box<dyn NativeObject>) -> Box<T> {
let raw: *mut dyn NativeObject = Box::into_raw(obj);

// SAFETY: We know that `obj` is of type `T` (due to the INVARIANT of `HostDefined`).
// See `HostDefined::insert`, `HostDefined::insert_default` and `HostDefined::remove`.
unsafe { Box::from_raw(raw.cast::<T>()) }
}

impl HostDefined {
/// Insert a type into the [`HostDefined`].
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
#[track_caller]
pub fn insert_default<T: NativeObject + Default>(&self) -> Option<Box<dyn NativeObject>> {
self.state
.borrow_mut()
pub fn insert_default<T: NativeObject + Default>(&mut self) -> Option<Box<T>> {
self.types
.insert(TypeId::of::<T>(), Box::<T>::default())
.map(downcast_boxed_native_object_unchecked)
}

/// Insert a type into the [`HostDefined`].
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
#[track_caller]
pub fn insert<T: NativeObject>(&self, value: T) -> Option<Box<dyn NativeObject>> {
self.state
.borrow_mut()
pub fn insert<T: NativeObject>(&mut self, value: T) -> Option<Box<T>> {
self.types
.insert(TypeId::of::<T>(), Box::new(value))
.map(downcast_boxed_native_object_unchecked)
}

/// Check if the [`HostDefined`] has type T.
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed mutably.
#[must_use]
#[track_caller]
pub fn has<T: NativeObject>(&self) -> bool {
self.state.borrow().contains_key(&TypeId::of::<T>())
self.types.contains_key(&TypeId::of::<T>())
}

/// Remove type T from [`HostDefined`], if it exists.
///
/// Returns [`Some`] with the object if it exits, [`None`] otherwise.
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
#[track_caller]
pub fn remove<T: NativeObject>(&self) -> Option<Box<dyn NativeObject>> {
self.state.borrow_mut().remove(&TypeId::of::<T>())
pub fn remove<T: NativeObject>(&mut self) -> Option<Box<T>> {
self.types
.remove(&TypeId::of::<T>())
.map(downcast_boxed_native_object_unchecked)
}

/// Get type T from [`HostDefined`], if it exits.
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
/// Get type T from [`HostDefined`], if it exists.
#[track_caller]
pub fn get<T: NativeObject>(&self) -> Option<GcRef<'_, T>> {
GcRef::try_map(self.state.borrow(), |state| {
state
.get(&TypeId::of::<T>())
.map(Box::as_ref)
.and_then(<dyn NativeObject>::downcast_ref::<T>)
})
pub fn get<T: NativeObject>(&self) -> Option<&T> {
self.types
.get(&TypeId::of::<T>())
.map(Box::as_ref)
.and_then(<dyn NativeObject>::downcast_ref::<T>)
}

/// Get type T from [`HostDefined`], if it exits.
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
/// Get type T from [`HostDefined`], if it exists.
#[track_caller]
pub fn get_mut<T: NativeObject>(&self) -> Option<GcRefMut<'_, HostDefinedMap, T>> {
GcRefMut::try_map(
self.state.borrow_mut(),
|state: &mut FxHashMap<TypeId, Box<dyn NativeObject>>| {
state
.get_mut(&TypeId::of::<T>())
.map(Box::as_mut)
.and_then(<dyn NativeObject>::downcast_mut::<T>)
},
)
pub fn get_mut<T: NativeObject>(&mut self) -> Option<&mut T> {
self.types
.get_mut(&TypeId::of::<T>())
.map(Box::as_mut)
.and_then(<dyn NativeObject>::downcast_mut::<T>)
}

/// Get type a tuple of types from [`HostDefined`], if they exist.
#[track_caller]
pub fn get_many_mut<T, const SIZE: usize>(&mut self) -> Option<T::NativeTupleMutRef<'_>>
where
T: NativeTuple<SIZE>,
{
let ids = T::as_type_ids();
let refs: [&TypeId; SIZE] = ids
.iter()
.collect::<Vec<_>>()
.try_into()
.expect("tuple should be of size `SIZE`");

self.types.get_many_mut(refs).and_then(T::mut_ref_from_anys)
}

/// Clears all the objects.
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
#[track_caller]
pub fn clear(&self) {
self.state.borrow_mut().clear();
pub fn clear(&mut self) {
self.types.clear();
}
}

/// This trait represents a tuple of [`NativeObject`]s capable of being
/// used in [`HostDefined`].
///
/// This allows accessing multiple types from [`HostDefined`] at once.
pub trait NativeTuple<const SIZE: usize> {
type NativeTupleMutRef<'a>;

fn as_type_ids() -> Vec<TypeId>;

fn mut_ref_from_anys(
anys: [&'_ mut Box<dyn NativeObject>; SIZE],
) -> Option<Self::NativeTupleMutRef<'_>>;
}

macro_rules! impl_native_tuple {
($size:literal $(,$name:ident)* ) => {
impl<$($name: NativeObject,)*> NativeTuple<$size> for ($($name,)*) {
type NativeTupleMutRef<'a> = ($(&'a mut $name,)*);

fn as_type_ids() -> Vec<TypeId> {
vec![$(TypeId::of::<$name>(),)*]
}

fn mut_ref_from_anys(
anys: [&'_ mut Box<dyn NativeObject>; $size],
) -> Option<Self::NativeTupleMutRef<'_>> {
#[allow(unused_variables, unused_mut)]
let mut anys = anys.into_iter();
Some(($(
anys.next().expect("Expect `anys` to be of length `SIZE`").downcast_mut::<$name>()?,
)*))
}
}
}
}

impl_native_tuple!(0);
impl_native_tuple!(1, A);
impl_native_tuple!(2, A, B);
impl_native_tuple!(3, A, B, C);
impl_native_tuple!(4, A, B, C, D);
impl_native_tuple!(5, A, B, C, D, E);
impl_native_tuple!(6, A, B, C, D, E, F);
impl_native_tuple!(7, A, B, C, D, E, F, G);
impl_native_tuple!(8, A, B, C, D, E, F, G, H);
impl_native_tuple!(9, A, B, C, D, E, F, G, H, I);
impl_native_tuple!(10, A, B, C, D, E, F, G, H, I, J);
impl_native_tuple!(11, A, B, C, D, E, F, G, H, I, J, K);
impl_native_tuple!(12, A, B, C, D, E, F, G, H, I, J, K, L);
31 changes: 25 additions & 6 deletions boa_engine/src/realm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
object::shape::RootShape,
HostDefined, JsObject, JsString,
};
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use boa_gc::{Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace};
use boa_profiler::Profiler;

/// Representation of a Realm.
Expand Down Expand Up @@ -61,7 +61,7 @@ struct Inner {
loaded_modules: GcRefCell<FxHashMap<JsString, Module>>,
host_classes: GcRefCell<FxHashMap<TypeId, StandardConstructor>>,

host_defined: HostDefined,
host_defined: GcRefCell<HostDefined>,
}

impl Realm {
Expand All @@ -86,7 +86,7 @@ impl Realm {
template_map: GcRefCell::default(),
loaded_modules: GcRefCell::default(),
host_classes: GcRefCell::default(),
host_defined: HostDefined::default(),
host_defined: GcRefCell::default(),
}),
};

Expand All @@ -102,13 +102,32 @@ impl Realm {
&self.inner.intrinsics
}

/// Returns the [`ECMAScript specification`][spec] defined [`\[\[\HostDefined]\]`][`HostDefined`] field of the [`Realm`].
/// Returns an immutable reference to the [`ECMAScript specification`][spec] defined
/// [`\[\[\HostDefined]\]`][`HostDefined`] field of the [`Realm`].
///
/// [spec]: https://tc39.es/ecma262/#table-realm-record-fields
///
/// # Panics
///
/// Panics if [`HostDefined`] field is mutably borrowed.
#[inline]
#[must_use]
pub fn host_defined(&self) -> GcRef<'_, HostDefined> {
self.inner.host_defined.borrow()
}

/// Returns a mutable reference to [`ECMAScript specification`][spec] defined
/// [`\[\[\HostDefined]\]`][`HostDefined`] field of the [`Realm`].
///
/// [spec]: https://tc39.es/ecma262/#table-realm-record-fields
///
/// # Panics
///
/// Panics if [`HostDefined`] field is borrowed.
#[inline]
#[must_use]
pub fn host_defined(&self) -> &HostDefined {
&self.inner.host_defined
pub fn host_defined_mut(&self) -> GcRefMut<'_, HostDefined> {
self.inner.host_defined.borrow_mut()
}

/// Checks if this `Realm` has the class `C` registered into its class map.
Expand Down
Loading

0 comments on commit 89fd3be

Please sign in to comment.