diff --git a/boa_engine/src/class.rs b/boa_engine/src/class.rs index 0c9c8cb5c09..ad4daedd4cc 100644 --- a/boa_engine/src/class.rs +++ b/boa_engine/src/class.rs @@ -1,14 +1,17 @@ //! Traits and structs for implementing native classes. //! //! Native classes are implemented through the [`Class`][class-trait] trait. +//! +//! # Examples +//! //! ``` //! # use boa_engine::{ //! # NativeFunction, //! # property::Attribute, //! # class::{Class, ClassBuilder}, //! # Context, JsResult, JsValue, -//! # JsArgs, -//! # js_string, +//! # JsArgs, Source, JsObject, js_string, +//! # JsNativeError, //! # }; //! # use boa_gc::{Finalize, Trace}; //! # @@ -24,11 +27,13 @@ //! // we set the binging name of this function to be `"Animal"`. //! const NAME: &'static str = "Animal"; //! -//! // We set the length to `1` since we accept 1 arguments in the constructor. -//! const LENGTH: usize = 1; +//! // We set the length to `2` since we accept 2 arguments in the constructor. +//! const LENGTH: usize = 2; //! //! // This is what is called when we do `new Animal()` to construct the inner data of the class. -//! fn make_data(_new_target: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { +//! // `_new_target` is the target of the `new` invocation, in this case the `Animal` constructor +//! // object. +//! fn data_constructor(_new_target: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { //! // This is equivalent to `String(arg)`. //! let kind = args.get_or_undefined(0).to_string(context)?; //! @@ -41,7 +46,22 @@ //! Ok(animal) //! } //! -//! /// This is where the object is initialized. +//! // This is also called on instance construction, but it receives the object wrapping the +//! // native data as its `instance` argument. +//! fn object_constructor( +//! instance: &JsObject, +//! args: &[JsValue], +//! context: &mut Context, +//! ) -> JsResult<()> { +//! let age = args.get_or_undefined(1).to_number(context)?; +//! +//! // Roughly equivalent to `this.age = Number(age)`. +//! instance.set(js_string!("age"), age, true, context)?; +//! +//! Ok(()) +//! } +//! +//! /// This is where the class object is initialized. //! fn init(class: &mut ClassBuilder) -> JsResult<()> { //! class.method( //! js_string!("speak"), @@ -49,19 +69,36 @@ //! NativeFunction::from_fn_ptr(|this, _args, _ctx| { //! if let Some(object) = this.as_object() { //! if let Some(animal) = object.downcast_ref::() { -//! match &*animal { -//! Self::Cat => println!("meow"), -//! Self::Dog => println!("woof"), -//! Self::Other => println!(r"¯\_(ツ)_/¯"), -//! } +//! return Ok(match &*animal { +//! Self::Cat => js_string!("meow"), +//! Self::Dog => js_string!("woof"), +//! Self::Other => js_string!(r"¯\_(ツ)_/¯"), +//! }.into()); //! } //! } -//! Ok(JsValue::undefined()) +//! Err(JsNativeError::typ().with_message("invalid this for class method").into()) //! }), //! ); //! Ok(()) //! } //! } +//! +//! fn main() { +//! let mut context = Context::default(); +//! +//! context.register_global_class::().unwrap(); +//! +//! let result = context.eval(Source::from_bytes(r#" +//! let pet = new Animal("dog", 3); +//! +//! `My pet is ${pet.age} years old. Right, buddy? - ${pet.speak()}!` +//! "#)).unwrap(); +//! +//! assert_eq!( +//! result.as_string().unwrap(), +//! &js_string!("My pet is 3 years old. Right, buddy? - woof!") +//! ); +//! } //! ``` //! //! [class-trait]: ./trait.Class.html @@ -79,6 +116,8 @@ use crate::{ }; /// Native class. +/// +/// See the [module-level documentation][self] for more details. pub trait Class: NativeObject + Sized { /// The binding name of this class. const NAME: &'static str; @@ -88,25 +127,42 @@ pub trait Class: NativeObject + Sized { /// Default is `writable`, `enumerable`, `configurable`. const ATTRIBUTES: Attribute = Attribute::all(); - /// Creates the internal data for an instance of this class. - /// - /// This method can also be called the "native constructor" of this class. - fn make_data(new_target: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult; - /// Initializes the properties and methods of this class. fn init(class: &mut ClassBuilder<'_>) -> JsResult<()>; - /// Creates a new [`JsObject`] with its internal data set to the result of calling `Self::make_data`. + /// Creates the internal data for an instance of this class. + fn data_constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult; + + /// Initializes the properties of the constructed object for an instance of this class. /// - /// # Note + /// Useful to initialize additional properties for the constructed object that aren't + /// stored inside the native data. + #[allow(unused_variables)] // Saves work when IDEs autocomplete trait impls. + fn object_constructor( + instance: &JsObject, + args: &[JsValue], + context: &mut Context, + ) -> JsResult<()> { + Ok(()) + } + + /// Creates a new [`JsObject`] with its internal data set to the result of calling + /// [`Class::data_constructor`] and [`Class::object_constructor`]. /// - /// This will throw an error if this class is not registered in the context's active realm. - /// See [`Context::register_global_class`]. + /// # Errors /// - /// # Warning + /// - Throws an error if `new_target` is undefined. + /// - Throws an error if this class is not registered in `new_target`'s realm. + /// See [`Context::register_global_class`]. /// + ///
/// Overriding this method could be useful for certain usages, but incorrectly implementing this /// could lead to weird errors like missing inherited methods or incorrect internal data. + ///
fn construct( new_target: &JsValue, args: &[JsValue], @@ -121,30 +177,72 @@ pub trait Class: NativeObject + Sized { .into()); } - let class = context.get_global_class::().ok_or_else(|| { - JsNativeError::typ().with_message(format!( - "could not find native class `{}` in the map of registered classes", - Self::NAME - )) - })?; + let prototype = 'proto: { + let realm = if let Some(constructor) = new_target.as_object() { + if let Some(proto) = constructor.get(PROTOTYPE, context)?.as_object() { + break 'proto proto.clone(); + } + constructor.get_function_realm(context)? + } else { + context.realm().clone() + }; + realm + .get_class::() + .ok_or_else(|| { + JsNativeError::typ().with_message(format!( + "could not find native class `{}` in the map of registered classes", + Self::NAME + )) + })? + .prototype() + }; + + let data = Self::data_constructor(new_target, args, context)?; + + let object = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::native_object(data), + ); + + Self::object_constructor(&object, args, context)?; - let prototype = new_target - .as_object() - .map(|obj| { - obj.get(PROTOTYPE, context) - .map(|val| val.as_object().cloned()) - }) - .transpose()? - .flatten() - .unwrap_or_else(|| class.prototype()); + Ok(object) + } - let data = Self::make_data(new_target, args, context)?; - let instance = JsObject::from_proto_and_data_with_shared_shape( + /// Constructs an instance of this class from its inner native data. + /// + /// Note that the default implementation won't call [`Class::data_constructor`], but it will + /// call [`Class::object_constructor`] with no arguments. + /// + /// # Errors + /// - Throws an error if this class is not registered in the context's realm. See + /// [`Context::register_global_class`]. + /// + ///
+ /// Overriding this method could be useful for certain usages, but incorrectly implementing this + /// could lead to weird errors like missing inherited methods or incorrect internal data. + ///
+ fn from_data(data: Self, context: &mut Context) -> JsResult { + let prototype = context + .get_global_class::() + .ok_or_else(|| { + JsNativeError::typ().with_message(format!( + "could not find native class `{}` in the map of registered classes", + Self::NAME + )) + })? + .prototype(); + + let object = JsObject::from_proto_and_data_with_shared_shape( context.root_shape(), prototype, ObjectData::native_object(data), ); - Ok(instance) + + Self::object_constructor(&object, &[], context)?; + + Ok(object) } } diff --git a/boa_engine/src/context/hooks.rs b/boa_engine/src/context/hooks.rs index 8af17cabe09..0fec62c967b 100644 --- a/boa_engine/src/context/hooks.rs +++ b/boa_engine/src/context/hooks.rs @@ -40,7 +40,8 @@ use super::intrinsics::Intrinsics; /// } /// } /// -/// let context = &mut ContextBuilder::new().host_hooks(&Hooks).build().unwrap(); +/// let context = +/// &mut ContextBuilder::new().host_hooks(&Hooks).build().unwrap(); /// let result = context.eval(Source::from_bytes(r#"eval("let a = 5")"#)); /// assert_eq!( /// result.unwrap_err().to_string(), diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index fac295af8d3..34c7fa7c433 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -252,14 +252,14 @@ pub trait JobQueue { /// can be done by passing it to the [`ContextBuilder`]: /// /// ``` -/// use std::rc::Rc; /// use boa_engine::{ /// context::ContextBuilder, /// job::{IdleJobQueue, JobQueue}, /// }; +/// use std::rc::Rc; /// /// let queue = Rc::new(IdleJobQueue); -/// let context = ContextBuilder::new().job_queue(queue ).build(); +/// let context = ContextBuilder::new().job_queue(queue).build(); /// ``` /// /// [`ContextBuilder`]: crate::context::ContextBuilder diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index c0e246027d3..2e984132647 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -2645,7 +2645,7 @@ impl<'ctx> ObjectInitializer<'ctx> { } /// Create a new `ObjectBuilder` with custom [`NativeObject`] data. - pub fn with_native(data: T, context: &'ctx mut Context) -> Self { + pub fn with_native_data(data: T, context: &'ctx mut Context) -> Self { let object = JsObject::from_proto_and_data_with_shared_shape( context.root_shape(), context.intrinsics().constructors().object().prototype(), @@ -2654,6 +2654,20 @@ impl<'ctx> ObjectInitializer<'ctx> { Self { context, object } } + /// Create a new `ObjectBuilder` with custom [`NativeObject`] data and custom prototype. + pub fn with_native_data_and_proto( + data: T, + proto: JsObject, + context: &'ctx mut Context, + ) -> Self { + let object = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + proto, + ObjectData::native_object(data), + ); + Self { context, object } + } + /// Add a function to the object. pub fn function(&mut self, function: NativeFunction, binding: B, length: usize) -> &mut Self where diff --git a/boa_examples/src/bin/classes.rs b/boa_examples/src/bin/classes.rs index c23d3da2bd9..49e98a0b565 100644 --- a/boa_examples/src/bin/classes.rs +++ b/boa_examples/src/bin/classes.rs @@ -65,7 +65,11 @@ impl Class for Person { const LENGTH: usize = 2; // This is what is internally called when we construct a `Person` with the expression `new Person()`. - fn make_data(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + fn data_constructor( + _this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { // We get the first argument. If it is unavailable we default to `undefined`, // and then we call `to_string()`. // diff --git a/boa_runtime/src/console/mod.rs b/boa_runtime/src/console/mod.rs index 51df8c067ba..05997d8aaba 100644 --- a/boa_runtime/src/console/mod.rs +++ b/boa_runtime/src/console/mod.rs @@ -163,7 +163,7 @@ impl Console { let state = Rc::new(RefCell::new(Self::default())); - ObjectInitializer::with_native(Self::default(), context) + ObjectInitializer::with_native_data(Self::default(), context) .function( console_method(Self::assert, state.clone()), js_string!("assert"),