diff --git a/boa_cli/src/debug/limits.rs b/boa_cli/src/debug/limits.rs index fd36d244f47..dfd8f8237b7 100644 --- a/boa_cli/src/debug/limits.rs +++ b/boa_cli/src/debug/limits.rs @@ -32,22 +32,24 @@ fn set_recursion(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> Js } pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { - let get_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_loop)) - .name("get loop") - .length(0) - .build(); - let set_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_loop)) - .name("set loop") - .length(1) - .build(); + let get_loop = + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(get_loop)) + .name("get loop") + .length(0) + .build(); + let set_loop = + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(set_loop)) + .name("set loop") + .length(1) + .build(); let get_recursion = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_recursion)) + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(get_recursion)) .name("get recursion") .length(0) .build(); let set_recursion = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_recursion)) + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(set_recursion)) .name("set recursion") .length(1) .build(); diff --git a/boa_cli/src/debug/optimizer.rs b/boa_cli/src/debug/optimizer.rs index 2580b776cd1..45c76993e65 100644 --- a/boa_cli/src/debug/optimizer.rs +++ b/boa_cli/src/debug/optimizer.rs @@ -44,24 +44,28 @@ fn set_statistics(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> J } pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { - let get_constant_folding = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_constant_folding)) - .name("get constantFolding") - .length(0) - .build(); - let set_constant_folding = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_constant_folding)) - .name("set constantFolding") - .length(1) - .build(); + let get_constant_folding = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(get_constant_folding), + ) + .name("get constantFolding") + .length(0) + .build(); + let set_constant_folding = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(set_constant_folding), + ) + .name("set constantFolding") + .length(1) + .build(); let get_statistics = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_statistics)) + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(get_statistics)) .name("get statistics") .length(0) .build(); let set_statistics = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_statistics)) + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(set_statistics)) .name("set statistics") .length(1) .build(); diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index 22cc8f532a6..e8504bc0c4c 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -550,7 +550,7 @@ impl AsyncGenerator { // 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: // 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, generator, context| { let next = { @@ -588,7 +588,7 @@ impl AsyncGenerator { // 9. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures generator and performs the following steps when called: // 10. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, generator, context| { let mut generator_borrow_mut = generator.borrow_mut(); diff --git a/boa_engine/src/builtins/function/tests.rs b/boa_engine/src/builtins/function/tests.rs index d5d87131fbd..565e4af2970 100644 --- a/boa_engine/src/builtins/function/tests.rs +++ b/boa_engine/src/builtins/function/tests.rs @@ -145,7 +145,7 @@ fn closure_capture_clone() { .unwrap(); let func = FunctionObjectBuilder::new( - ctx, + ctx.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, captures, context| { let (string, object) = &captures; diff --git a/boa_engine/src/builtins/intl/collator/mod.rs b/boa_engine/src/builtins/intl/collator/mod.rs index af88d1cb30c..95dfc94370c 100644 --- a/boa_engine/src/builtins/intl/collator/mod.rs +++ b/boa_engine/src/builtins/intl/collator/mod.rs @@ -443,7 +443,7 @@ impl Collator { f } else { let bound_compare = FunctionObjectBuilder::new( - context, + context.realm(), // 10.3.3.1. Collator Compare Functions // https://tc39.es/ecma402/#sec-collator-compare-functions NativeFunction::from_copy_closure_with_captures( diff --git a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs index 45098226af0..c59c72af4d3 100644 --- a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -319,7 +319,7 @@ impl AsyncFromSyncIterator { // that captures done and performs the following steps when called: // 9. Let onFulfilled be CreateBuiltinFunction(unwrap, 1, "", « »). let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure(move |_this, args, context| { // a. Return CreateIterResultObject(value, done). Ok(create_iter_result_object( diff --git a/boa_engine/src/builtins/object/mod.rs b/boa_engine/src/builtins/object/mod.rs index 0f40d5e43eb..d848bcfe865 100644 --- a/boa_engine/src/builtins/object/mod.rs +++ b/boa_engine/src/builtins/object/mod.rs @@ -1283,7 +1283,7 @@ impl Object { // 4. Let closure be a new Abstract Closure with parameters (key, value) that captures // obj and performs the following steps when called: let closure = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, obj, context| { let key = args.get_or_undefined(0); diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index c9eb2e32554..280e1191b19 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -229,7 +229,7 @@ impl PromiseCapability { // 4. Let executorClosure be a new Abstract Closure with parameters (resolve, reject) that captures promiseCapability and performs the following steps when called: // 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »). let executor = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args: &[JsValue], captures, _| { let mut promise_capability = captures.borrow_mut(); @@ -595,7 +595,7 @@ impl Promise { // p. Set onFulfilled.[[Capability]] to resultCapability. // q. Set onFulfilled.[[RemainingElements]] to remainingElementsCount. let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, captures, context| { // https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions @@ -820,7 +820,7 @@ impl Promise { // q. Set onFulfilled.[[Capability]] to resultCapability. // r. Set onFulfilled.[[RemainingElements]] to remainingElementsCount. let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, captures, context| { // https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions @@ -906,7 +906,7 @@ impl Promise { // y. Set onRejected.[[Capability]] to resultCapability. // z. Set onRejected.[[RemainingElements]] to remainingElementsCount. let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, captures, context| { // https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions @@ -1149,7 +1149,7 @@ impl Promise { // p. Set onRejected.[[Capability]] to resultCapability. // q. Set onRejected.[[RemainingElements]] to remainingElementsCount. let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, captures, context| { // https://tc39.es/ecma262/#sec-promise.any-reject-element-functions @@ -1567,7 +1567,7 @@ impl Promise { // a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called: let then_finally_closure = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { /// Capture object for the abstract `returnValue` closure. @@ -1588,7 +1588,7 @@ impl Promise { // iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called: let return_value = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, _args, captures, _context| { // 1. Return value. @@ -1618,7 +1618,7 @@ impl Promise { // c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called: let catch_finally_closure = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { /// Capture object for the abstract `throwReason` closure. @@ -1639,7 +1639,7 @@ impl Promise { // iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: let throw_reason = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, _args, captures, _context| { // 1. Return ThrowCompletion(reason). @@ -2020,7 +2020,7 @@ impl Promise { // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). let resolve = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { // https://tc39.es/ecma262/#sec-promise-resolve-functions @@ -2119,7 +2119,7 @@ impl Promise { // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). let reject = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { // https://tc39.es/ecma262/#sec-promise-reject-functions diff --git a/boa_engine/src/builtins/proxy/mod.rs b/boa_engine/src/builtins/proxy/mod.rs index 8e754c589ad..9324ea6713f 100644 --- a/boa_engine/src/builtins/proxy/mod.rs +++ b/boa_engine/src/builtins/proxy/mod.rs @@ -143,7 +143,7 @@ impl Proxy { // 3. Let revoker be ! CreateBuiltinFunction(revokerClosure, 0, "", « [[RevocableProxy]] »). // 4. Set revoker.[[RevocableProxy]] to p. FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, revocable_proxy, _| { // a. Let F be the active function object. diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index f57e9ef3c3f..5666726ff7c 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -256,7 +256,7 @@ impl<'host> Context<'host> { length: usize, body: NativeFunction, ) -> JsResult<()> { - let function = FunctionObjectBuilder::new(self, body) + let function = FunctionObjectBuilder::new(&self.realm, body) .name(name) .length(length) .constructor(true) @@ -289,7 +289,7 @@ impl<'host> Context<'host> { length: usize, body: NativeFunction, ) -> JsResult<()> { - let function = FunctionObjectBuilder::new(self, body) + let function = FunctionObjectBuilder::new(&self.realm, body) .name(name) .length(length) .constructor(false) diff --git a/boa_engine/src/host_defined.rs b/boa_engine/src/host_defined.rs new file mode 100644 index 00000000000..f56e830dc63 --- /dev/null +++ b/boa_engine/src/host_defined.rs @@ -0,0 +1,127 @@ +use std::any::TypeId; + +use boa_gc::{GcRef, GcRefCell, GcRefMut}; +use boa_macros::{Finalize, Trace}; +use rustc_hash::FxHashMap; + +use crate::object::NativeObject; + +/// Map used to store the host defined objects. +#[doc(hidden)] +type HostDefinedMap = FxHashMap>; + +/// 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, +} + +impl HostDefined { + /// Insert a type into the [`HostDefined`]. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed. + #[track_caller] + pub fn insert_default(&self) -> Option> { + self.state + .borrow_mut() + .insert(TypeId::of::(), Box::::default()) + } + + /// Insert a type into the [`HostDefined`]. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed. + #[track_caller] + pub fn insert(&self, value: T) -> Option> { + self.state + .borrow_mut() + .insert(TypeId::of::(), Box::new(value)) + } + + /// Check if the [`HostDefined`] has type T. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed mutably. + #[track_caller] + pub fn has(&self) -> bool { + self.state.borrow().contains_key(&TypeId::of::()) + } + + /// 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(&self) -> Option> { + self.state.borrow_mut().remove(&TypeId::of::()) + } + + /// Get type T from [`HostDefined`], if it exits. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed. + #[track_caller] + pub fn get(&self) -> Option> { + let state = self.state.borrow(); + + state + .get(&TypeId::of::()) + .map(Box::as_ref) + .and_then(::downcast_ref::)?; + + Some(GcRef::map(state, |state| { + state + .get(&TypeId::of::()) + .map(Box::as_ref) + .and_then(::downcast_ref::) + .expect("Should not fail") + })) + } + + /// Get type T from [`HostDefined`], if it exits. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed. + #[track_caller] + pub fn get_mut(&self) -> Option> { + let mut state = self.state.borrow_mut(); + + state + .get_mut(&TypeId::of::()) + .map(Box::as_mut) + .and_then(::downcast_mut::)?; + + Some(GcRefMut::map( + state, + |state: &mut FxHashMap>| { + state + .get_mut(&TypeId::of::()) + .map(Box::as_mut) + .and_then(::downcast_mut::) + .expect("Should not fail") + }, + )) + } + + /// Clears all the objects. + /// + /// # Panics + /// + /// Panics if [`HostDefined`] field is borrowed. + #[track_caller] + pub fn clear(&self) { + self.state.borrow_mut().clear(); + } +} diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 34373bcaf8c..7d92fd615bc 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -17,7 +17,7 @@ //! [Job]: https://tc39.es/ecma262/#sec-jobs //! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records -use std::{any::Any, cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin}; +use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin}; use crate::{ object::{JsFunction, NativeObject}, @@ -160,7 +160,8 @@ impl Debug for JobCallback { impl JobCallback { /// Creates a new `JobCallback`. - pub fn new(callback: JsFunction, host_defined: T) -> Self { + #[inline] + pub fn new(callback: JsFunction, host_defined: T) -> Self { Self { callback, host_defined: Box::new(host_defined), @@ -168,20 +169,22 @@ impl JobCallback { } /// Gets the inner callback of the job. + #[inline] #[must_use] pub const fn callback(&self) -> &JsFunction { &self.callback } - /// Gets a reference to the host defined additional field as an `Any` trait object. - #[must_use] - pub fn host_defined(&self) -> &dyn Any { - self.host_defined.as_any() + /// Gets a reference to the host defined additional field as an [`NativeObject`] trait object. + #[inline] + pub fn host_defined(&self) -> &dyn NativeObject { + &*self.host_defined } - /// Gets a mutable reference to the host defined additional field as an `Any` trait object. - pub fn host_defined_mut(&mut self) -> &mut dyn Any { - self.host_defined.as_mut_any() + /// Gets a mutable reference to the host defined additional field as an [`NativeObject`] trait object. + #[inline] + pub fn host_defined_mut(&mut self) -> &mut dyn NativeObject { + &mut *self.host_defined } } diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 9a7eb0c60ae..332ed46227f 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -144,12 +144,14 @@ pub mod realm; pub mod script; pub mod string; pub mod symbol; -// pub(crate) mod tagged; +pub mod value; +pub mod vm; + +mod host_defined; mod tagged; + #[cfg(test)] mod tests; -pub mod value; -pub mod vm; /// A convenience module that re-exports the most commonly-used Boa APIs pub mod prelude { @@ -172,6 +174,7 @@ pub use crate::{ bigint::JsBigInt, context::Context, error::{JsError, JsNativeError, JsNativeErrorKind}, + host_defined::HostDefined, module::Module, native_function::NativeFunction, object::JsObject, diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index c8082a28e46..026d7ff2068 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -652,7 +652,7 @@ impl Module { .then( Some( FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, module, context| { module.link(context)?; @@ -669,7 +669,7 @@ impl Module { .then( Some( FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, module, context| Ok(module.evaluate(context).into()), self.clone(), diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 16eb4ecfce7..4daef440e7c 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1267,7 +1267,7 @@ impl SourceTextModule { // 4. Let fulfilledClosure be a new Abstract Closure with no parameters that captures module and performs the following steps when called: // 5. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »). let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, module, context| { // a. Perform AsyncModuleExecutionFulfilled(module). @@ -1283,7 +1283,7 @@ impl SourceTextModule { // 6. Let rejectedClosure be a new Abstract Closure with parameters (error) that captures module and performs the following steps when called: // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, module, context| { let error = JsError::from_opaque(args.get_or_undefined(0).clone()); diff --git a/boa_engine/src/object/builtins/jspromise.rs b/boa_engine/src/object/builtins/jspromise.rs index e596eabd64d..b674706fe45 100644 --- a/boa_engine/src/object/builtins/jspromise.rs +++ b/boa_engine/src/object/builtins/jspromise.rs @@ -52,7 +52,7 @@ use boa_gc::{Finalize, Gc, GcRefCell, Trace}; /// .then( /// Some( /// FunctionObjectBuilder::new( -/// context, +/// context.realm(), /// NativeFunction::from_fn_ptr(|_, args, _| { /// Err(JsError::from_opaque(args.get_or_undefined(0).clone()).into()) /// }), @@ -64,7 +64,7 @@ use boa_gc::{Finalize, Gc, GcRefCell, Trace}; /// )? /// .catch( /// FunctionObjectBuilder::new( -/// context, +/// context.realm(), /// NativeFunction::from_fn_ptr(|_, args, _| { /// Ok(args.get_or_undefined(0).clone()) /// }), @@ -74,7 +74,7 @@ use boa_gc::{Finalize, Gc, GcRefCell, Trace}; /// )? /// .finally( /// FunctionObjectBuilder::new( -/// context, +/// context.realm(), /// NativeFunction::from_fn_ptr(|_, _, context| { /// context /// .global_object() @@ -463,7 +463,7 @@ impl JsPromise { /// )?.then( /// Some( /// FunctionObjectBuilder::new( - /// context, + /// context.realm(), /// NativeFunction::from_fn_ptr(|_, args, context| { /// args.get_or_undefined(0).to_string(context).map(JsValue::from) /// }), @@ -529,7 +529,7 @@ impl JsPromise { /// context, /// )?.catch( /// FunctionObjectBuilder::new( - /// context, + /// context.realm(), /// NativeFunction::from_fn_ptr(|_, args, context| { /// args.get_or_undefined(0).to_string(context).map(JsValue::from) /// }), @@ -593,7 +593,7 @@ impl JsPromise { /// context, /// )?.finally( /// FunctionObjectBuilder::new( - /// context, + /// context.realm(), /// NativeFunction::from_fn_ptr(|_, _, context| { /// context /// .global_object() diff --git a/boa_engine/src/object/builtins/jsproxy.rs b/boa_engine/src/object/builtins/jsproxy.rs index 575263562c4..b3d005c2544 100644 --- a/boa_engine/src/object/builtins/jsproxy.rs +++ b/boa_engine/src/object/builtins/jsproxy.rs @@ -401,7 +401,7 @@ impl JsProxyBuilder { let handler = JsObject::with_object_proto(context.intrinsics()); if let Some(apply) = self.apply { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(apply)) + let f = FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(apply)) .length(3) .build(); handler @@ -409,33 +409,38 @@ impl JsProxyBuilder { .expect("new object should be writable"); } if let Some(construct) = self.construct { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(construct)) - .length(3) - .build(); + let f = + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(construct)) + .length(3) + .build(); handler .create_data_property_or_throw(utf16!("construct"), f, context) .expect("new object should be writable"); } if let Some(define_property) = self.define_property { - let f = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(define_property)) - .length(3) - .build(); + let f = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(define_property), + ) + .length(3) + .build(); handler .create_data_property_or_throw(utf16!("defineProperty"), f, context) .expect("new object should be writable"); } if let Some(delete_property) = self.delete_property { - let f = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(delete_property)) - .length(2) - .build(); + let f = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(delete_property), + ) + .length(2) + .build(); handler .create_data_property_or_throw(utf16!("deleteProperty"), f, context) .expect("new object should be writable"); } if let Some(get) = self.get { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get)) + let f = FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(get)) .length(3) .build(); handler @@ -444,7 +449,7 @@ impl JsProxyBuilder { } if let Some(get_own_property_descriptor) = self.get_own_property_descriptor { let f = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(get_own_property_descriptor), ) .length(2) @@ -454,16 +459,18 @@ impl JsProxyBuilder { .expect("new object should be writable"); } if let Some(get_prototype_of) = self.get_prototype_of { - let f = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_prototype_of)) - .length(1) - .build(); + let f = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(get_prototype_of), + ) + .length(1) + .build(); handler .create_data_property_or_throw(utf16!("getPrototypeOf"), f, context) .expect("new object should be writable"); } if let Some(has) = self.has { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(has)) + let f = FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(has)) .length(2) .build(); handler @@ -471,24 +478,28 @@ impl JsProxyBuilder { .expect("new object should be writable"); } if let Some(is_extensible) = self.is_extensible { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(is_extensible)) - .length(1) - .build(); + let f = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(is_extensible), + ) + .length(1) + .build(); handler .create_data_property_or_throw(utf16!("isExtensible"), f, context) .expect("new object should be writable"); } if let Some(own_keys) = self.own_keys { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(own_keys)) - .length(1) - .build(); + let f = + FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(own_keys)) + .length(1) + .build(); handler .create_data_property_or_throw(utf16!("ownKeys"), f, context) .expect("new object should be writable"); } if let Some(prevent_extensions) = self.prevent_extensions { let f = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(prevent_extensions), ) .length(1) @@ -498,7 +509,7 @@ impl JsProxyBuilder { .expect("new object should be writable"); } if let Some(set) = self.set { - let f = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set)) + let f = FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(set)) .length(4) .build(); handler @@ -506,10 +517,12 @@ impl JsProxyBuilder { .expect("new object should be writable"); } if let Some(set_prototype_of) = self.set_prototype_of { - let f = - FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_prototype_of)) - .length(2) - .build(); + let f = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(set_prototype_of), + ) + .length(2) + .build(); handler .create_data_property_or_throw(utf16!("setPrototypeOf"), f, context) .expect("new object should be writable"); diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a7e4db77fad..6ec821996be 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -59,13 +59,14 @@ use crate::{ module::ModuleNamespace, native_function::NativeFunction, property::{Attribute, PropertyDescriptor, PropertyKey}, + realm::Realm, string::utf16, Context, JsBigInt, JsString, JsSymbol, JsValue, }; use boa_gc::{custom_trace, Finalize, Trace, WeakGc}; use std::{ - any::Any, + any::{Any, TypeId}, fmt::{self, Debug}, ops::{Deref, DerefMut}, }; @@ -129,6 +130,75 @@ impl NativeObject for T { } } +impl dyn NativeObject { + /// Returns `true` if the inner type is the same as `T`. + #[inline] + pub fn is(&self) -> bool { + // Get `TypeId` of the type this function is instantiated with. + let t = TypeId::of::(); + + // Get `TypeId` of the type in the trait object (`self`). + let concrete = self.type_id(); + + // Compare both `TypeId`s on equality. + t == concrete + } + + /// Returns some reference to the inner value if it is of type `T`, or + /// `None` if it isn't. + #[inline] + pub fn downcast_ref(&self) -> Option<&T> { + if self.is::() { + // SAFETY: just checked whether we are pointing to the correct type, and we can rely on + // that check for memory safety because we have implemented NativeObject for all types; no other + // impls can exist as they would conflict with our impl. + unsafe { Some(self.downcast_ref_unchecked()) } + } else { + None + } + } + + /// Returns some mutable reference to the inner value if it is of type `T`, or + /// `None` if it isn't. + #[inline] + pub fn downcast_mut(&mut self) -> Option<&mut T> { + if self.is::() { + // SAFETY: Already checked if inner type is T, so this is safe. + unsafe { Some(self.downcast_mut_unchecked()) } + } else { + None + } + } + + /// Returns a reference to the inner value as type `dyn T`. + /// + /// # Safety + /// + /// The contained value must be of type `T`. Calling this method + /// with the incorrect type is *undefined behavior*. + #[inline] + pub unsafe fn downcast_ref_unchecked(&self) -> &T { + debug_assert!(self.is::()); + let ptr: *const dyn NativeObject = self; + // SAFETY: caller guarantees that T is the correct type + unsafe { &*ptr.cast::() } + } + + /// Returns a mutable reference to the inner value as type `dyn T`. + /// + /// # Safety + /// + /// The contained value must be of type `T`. Calling this method + /// with the incorrect type is *undefined behavior*. + #[inline] + pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut T { + debug_assert!(self.is::()); + // SAFETY: caller guarantees that T is the correct type + let ptr: *mut dyn NativeObject = self; + unsafe { &mut *ptr.cast::() } + } +} + /// The internal representation of a JavaScript object. #[derive(Debug, Finalize)] pub struct Object { @@ -1951,20 +2021,20 @@ where /// Builder for creating native function objects #[derive(Debug)] -pub struct FunctionObjectBuilder<'ctx, 'host> { - context: &'ctx mut Context<'host>, +pub struct FunctionObjectBuilder<'realm> { + realm: &'realm Realm, function: NativeFunction, constructor: Option, name: JsString, length: usize, } -impl<'ctx, 'host> FunctionObjectBuilder<'ctx, 'host> { +impl<'realm> FunctionObjectBuilder<'realm> { /// Create a new `FunctionBuilder` for creating a native function. #[inline] - pub fn new(context: &'ctx mut Context<'host>, function: NativeFunction) -> Self { + pub fn new(realm: &'realm Realm, function: NativeFunction) -> Self { Self { - context, + realm, function, constructor: None, name: js_string!(), @@ -2013,9 +2083,9 @@ impl<'ctx, 'host> FunctionObjectBuilder<'ctx, 'host> { function: self.function, constructor: self.constructor, }, - self.context.realm().clone(), + self.realm.clone(), ); - let object = self.context.intrinsics().templates().function().create( + let object = self.realm.intrinsics().templates().function().create( ObjectData::function(function, self.constructor.is_some()), vec![self.length.into(), self.name.into()], ); @@ -2082,7 +2152,7 @@ impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> { B: Into, { let binding = binding.into(); - let function = FunctionObjectBuilder::new(self.context, function) + let function = FunctionObjectBuilder::new(self.context.realm(), function) .name(binding.name) .length(length) .constructor(false) @@ -2202,7 +2272,7 @@ impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> { B: Into, { let binding = binding.into(); - let function = FunctionObjectBuilder::new(self.context, function) + let function = FunctionObjectBuilder::new(self.context.realm(), function) .name(binding.name) .length(length) .constructor(false) @@ -2230,7 +2300,7 @@ impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> { B: Into, { let binding = binding.into(); - let function = FunctionObjectBuilder::new(self.context, function) + let function = FunctionObjectBuilder::new(self.context.realm(), function) .name(binding.name) .length(length) .constructor(false) diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 65237b6c049..229b2df0759 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -10,8 +10,8 @@ use crate::{ context::{intrinsics::Intrinsics, HostHooks}, environments::DeclarativeEnvironment, module::Module, - object::{shape::RootShape, JsObject}, - JsString, + object::shape::RootShape, + HostDefined, JsObject, JsString, }; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; use boa_profiler::Profiler; @@ -52,10 +52,12 @@ struct Inner { global_this: JsObject, template_map: GcRefCell>, loaded_modules: GcRefCell>, + + host_defined: HostDefined, } impl Realm { - /// Create a new Realm. + /// Create a new [`Realm`]. #[inline] pub fn create(hooks: &dyn HostHooks, root_shape: &RootShape) -> Self { let _timer = Profiler::global().start_event("Realm::create", "realm"); @@ -75,6 +77,7 @@ impl Realm { global_this, template_map: GcRefCell::default(), loaded_modules: GcRefCell::default(), + host_defined: HostDefined::default(), }), }; @@ -84,11 +87,20 @@ impl Realm { } /// Gets the intrinsics of this `Realm`. + #[inline] #[must_use] pub fn intrinsics(&self) -> &Intrinsics { &self.inner.intrinsics } + /// Returns the [`ECMAScript specification`][spec] defined [`\[\[\HostDefined]\]`][`HostDefined`] field of the [`Realm`]. + /// + /// [spec]: https://tc39.es/ecma262/#table-realm-record-fields + #[inline] + pub fn host_defined(&self) -> &HostDefined { + &self.inner.host_defined + } + pub(crate) fn environment(&self) -> &Gc { &self.inner.environment } diff --git a/boa_engine/src/value/conversions/try_from_js.rs b/boa_engine/src/value/conversions/try_from_js.rs index bfd44936cbc..975881d4a74 100644 --- a/boa_engine/src/value/conversions/try_from_js.rs +++ b/boa_engine/src/value/conversions/try_from_js.rs @@ -210,6 +210,21 @@ impl TryFromJs for u64 { } } +impl TryFromJs for usize { + fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + JsNativeError::typ() + .with_message(format!("cannot convert value to a usize: {e}")) + .into() + }), + _ => Err(JsNativeError::typ() + .with_message("cannot convert value to a usize") + .into()), + } + } +} + impl TryFromJs for i128 { fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult { match value { diff --git a/boa_engine/src/vm/opcode/await/mod.rs b/boa_engine/src/vm/opcode/await/mod.rs index c39e42128a4..d865077f256 100644 --- a/boa_engine/src/vm/opcode/await/mod.rs +++ b/boa_engine/src/vm/opcode/await/mod.rs @@ -36,7 +36,7 @@ impl Operation for Await { // 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called: // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). let on_fulfilled = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { // a. Let prevContext be the running execution context. @@ -77,7 +77,7 @@ impl Operation for Await { // 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called: // 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_this, args, captures, context| { // a. Let prevContext be the running execution context. diff --git a/boa_engine/src/vm/opcode/call/mod.rs b/boa_engine/src/vm/opcode/call/mod.rs index 4bdf52d7d94..ba1a46f6000 100644 --- a/boa_engine/src/vm/opcode/call/mod.rs +++ b/boa_engine/src/vm/opcode/call/mod.rs @@ -368,7 +368,7 @@ impl Operation for ImportCall { // 4. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures promiseCapability and performs the following steps when called: // 5. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). let on_rejected = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, args, cap, context| { // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « reason »). @@ -387,7 +387,7 @@ impl Operation for ImportCall { // 6. Let linkAndEvaluateClosure be a new Abstract Closure with no parameters that captures module, promiseCapability, and onRejected and performs the following steps when called: // 7. Let linkAndEvaluate be CreateBuiltinFunction(linkAndEvaluateClosure, 0, "", « »). let link_evaluate = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, (module, cap, on_rejected), context| { // a. Let link be Completion(module.Link()). @@ -408,7 +408,7 @@ impl Operation for ImportCall { // d. Let fulfilledClosure be a new Abstract Closure with no parameters that captures module and promiseCapability and performs the following steps when called: // e. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »). let fulfill = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, (module, cap), context| { // i. Let namespace be GetModuleNamespace(module). diff --git a/boa_examples/src/bin/closures.rs b/boa_examples/src/bin/closures.rs index 14eb3d0b127..02ff0763352 100644 --- a/boa_examples/src/bin/closures.rs +++ b/boa_examples/src/bin/closures.rs @@ -71,7 +71,7 @@ fn main() -> Result<(), JsError> { // We can use `FunctionBuilder` to define a closure with additional captures and custom property // attributes. let js_function = FunctionObjectBuilder::new( - &mut context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, captures, context| { let mut captures = captures.borrow_mut(); diff --git a/boa_examples/src/bin/host_defined.rs b/boa_examples/src/bin/host_defined.rs new file mode 100644 index 00000000000..b90db74e328 --- /dev/null +++ b/boa_examples/src/bin/host_defined.rs @@ -0,0 +1,135 @@ +// This example goes into the details on how to store user defined structs/state that is shared. + +use boa_engine::{ + native_function::NativeFunction, Context, JsArgs, JsError, JsNativeError, Source, +}; +use boa_gc::{Finalize, Trace}; + +/// Custom host-defined struct that has some state, and can be shared between JavaScript and rust. +#[derive(Default, Trace, Finalize)] +struct CustomHostDefinedStruct { + #[unsafe_ignore_trace] + counter: usize, +} + +/// Custom host-defined struct that has some state, and can be shared between JavaScript and rust. +#[derive(Trace, Finalize)] +struct AnotherCustomHostDefinedStruct { + #[unsafe_ignore_trace] + counter: usize, +} + +impl AnotherCustomHostDefinedStruct { + fn new(value: usize) -> Self { + Self { counter: value } + } +} + +fn main() -> Result<(), JsError> { + // We create a new `Context` to create a new Javascript executor.. + let mut context = Context::default(); + + // Get the realm from the context. + let realm = context.realm().clone(); + + // Insert a default CustomHostDefinedStruct. + realm + .host_defined() + .insert_default::(); + + { + assert!(realm.host_defined().has::()); + + // Get the host define field from the realm and downcast it to our concrete type. + let Some(host_defined) = realm.host_defined().get::() else { + return Err(JsNativeError::typ() + .with_message("Realm does not HostDefined field") + .into()); + }; + + // Assert that the initial state. + assert_eq!(host_defined.counter, 0); + } + + // Insert another struct with state into [[HostDefined]] field. + realm + .host_defined() + .insert(AnotherCustomHostDefinedStruct::new(10)); + + { + assert!(realm.host_defined().has::()); + + // Get the host define field from the realm and downcast it to our concrete type. + let Some(host_defined) = realm.host_defined().get::() + else { + return Err(JsNativeError::typ() + .with_message("Realm does not HostDefined field") + .into()); + }; + + // Assert that the initial state. + assert_eq!(host_defined.counter, 10); + } + + // Remove a type from the [[HostDefined]] field. + assert!(realm + .host_defined() + .remove::() + .is_some()); + + // Create and register function for setting and setting the realm value. + // + // The funtion lives in the context's realm and has access to the host-defined field. + context.register_global_builtin_callable( + "setRealmValue", + 1, + NativeFunction::from_fn_ptr(|_, args, context| { + let value: usize = args.get_or_undefined(0).try_js_into(context)?; + + let host_defined = context.realm().host_defined(); + let Some(mut host_defined) = host_defined.get_mut::() else { + return Err(JsNativeError::typ() + .with_message("Realm does not HostDefined field") + .into()); + }; + + host_defined.counter = value; + + Ok(value.into()) + }), + )?; + + context.register_global_builtin_callable( + "getRealmValue", + 0, + NativeFunction::from_fn_ptr(|_, _, context| { + let host_defined = context.realm().host_defined(); + let Some(host_defined) = host_defined.get::() else { + return Err(JsNativeError::typ() + .with_message("Realm does not HostDefined field") + .into()); + }; + + Ok(host_defined.counter.into()) + }), + )?; + + // Run code in JavaScript that mutates the host-defined field on the Realm. + context.eval(Source::from_bytes( + r" + setRealmValue(50); + setRealmValue(getRealmValue() * 2); + ", + ))?; + + let Some(host_defined) = realm.host_defined().get::() else { + return Err(JsNativeError::typ() + .with_message("Realm does not HostDefined field") + .into()); + }; + + // Assert that the host-defined field changed. + assert_eq!(host_defined.counter, 100); + + Ok(()) +} diff --git a/boa_examples/src/bin/jsarray.rs b/boa_examples/src/bin/jsarray.rs index 7088d18d83a..8d8578d4172 100644 --- a/boa_examples/src/bin/jsarray.rs +++ b/boa_examples/src/bin/jsarray.rs @@ -62,7 +62,7 @@ fn main() -> JsResult<()> { assert_eq!(&joined_array, utf16!("14::false::false::false::10")); let filter_callback = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(|_this, args, _context| { Ok(args.get(0).cloned().unwrap_or_default().is_number().into()) }), @@ -70,7 +70,7 @@ fn main() -> JsResult<()> { .build(); let map_callback = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(|_this, args, context| { args.get(0) .cloned() @@ -96,7 +96,7 @@ fn main() -> JsResult<()> { assert_eq!(&chained_array.join(None, context)?, utf16!("196,1,2,3")); let reduce_callback = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(|_this, args, context| { let accumulator = args.get(0).cloned().unwrap_or_default(); let value = args.get(1).cloned().unwrap_or_default(); diff --git a/boa_examples/src/bin/jstypedarray.rs b/boa_examples/src/bin/jstypedarray.rs index e5c31611c23..1da60569207 100644 --- a/boa_examples/src/bin/jstypedarray.rs +++ b/boa_examples/src/bin/jstypedarray.rs @@ -25,7 +25,7 @@ fn main() -> JsResult<()> { } let callback = FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_fn_ptr(|_this, args, context| { let accumulator = args.get(0).cloned().unwrap_or_default(); let value = args.get(1).cloned().unwrap_or_default(); diff --git a/boa_examples/src/bin/modules.rs b/boa_examples/src/bin/modules.rs index 2d9435adf08..df9e091e647 100644 --- a/boa_examples/src/bin/modules.rs +++ b/boa_examples/src/bin/modules.rs @@ -58,7 +58,7 @@ fn main() -> Result<(), Box> { .then( Some( FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( |_, _, module, context| { // After loading, link all modules by resolving the imports @@ -79,7 +79,7 @@ fn main() -> Result<(), Box> { .then( Some( FunctionObjectBuilder::new( - context, + context.realm(), NativeFunction::from_copy_closure_with_captures( // Finally, evaluate the root module. // This returns a `JsPromise` since a module could have diff --git a/boa_gc/src/trace.rs b/boa_gc/src/trace.rs index a1b57171687..ba5704bed74 100644 --- a/boa_gc/src/trace.rs +++ b/boa_gc/src/trace.rs @@ -1,4 +1,5 @@ use std::{ + any::TypeId, borrow::{Cow, ToOwned}, cell::Cell, collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}, @@ -143,6 +144,7 @@ simple_empty_finalize_trace![ f32, f64, char, + TypeId, String, Box, Rc, diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index f96f3bf5008..4d69fd044eb 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -622,7 +622,7 @@ fn is_error_type(error: &JsError, target_type: ErrorType, context: &mut Context< fn register_print_fn(context: &mut Context<'_>, async_result: AsyncResult) { // We use `FunctionBuilder` to define a closure with additional captures. let js_function = FunctionObjectBuilder::new( - context, + context.realm(), // SAFETY: `AsyncResult` has only non-traceable captures, making this safe. unsafe { NativeFunction::from_closure(move |_, args, context| {