Browse Source

Add utility methods to the `Class` trait (#3488)

pull/3491/head
José Julián Espina 11 months ago committed by GitHub
parent
commit
9f181ef3aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 170
      boa_engine/src/class.rs
  2. 3
      boa_engine/src/context/hooks.rs
  3. 4
      boa_engine/src/job.rs
  4. 16
      boa_engine/src/object/mod.rs
  5. 6
      boa_examples/src/bin/classes.rs
  6. 2
      boa_runtime/src/console/mod.rs

170
boa_engine/src/class.rs

@ -1,14 +1,17 @@
//! Traits and structs for implementing native classes. //! Traits and structs for implementing native classes.
//! //!
//! Native classes are implemented through the [`Class`][class-trait] trait. //! Native classes are implemented through the [`Class`][class-trait] trait.
//!
//! # Examples
//!
//! ``` //! ```
//! # use boa_engine::{ //! # use boa_engine::{
//! # NativeFunction, //! # NativeFunction,
//! # property::Attribute, //! # property::Attribute,
//! # class::{Class, ClassBuilder}, //! # class::{Class, ClassBuilder},
//! # Context, JsResult, JsValue, //! # Context, JsResult, JsValue,
//! # JsArgs, //! # JsArgs, Source, JsObject, js_string,
//! # js_string, //! # JsNativeError,
//! # }; //! # };
//! # use boa_gc::{Finalize, Trace}; //! # use boa_gc::{Finalize, Trace};
//! # //! #
@ -24,11 +27,13 @@
//! // we set the binging name of this function to be `"Animal"`. //! // we set the binging name of this function to be `"Animal"`.
//! const NAME: &'static str = "Animal"; //! const NAME: &'static str = "Animal";
//! //!
//! // We set the length to `1` since we accept 1 arguments in the constructor. //! // We set the length to `2` since we accept 2 arguments in the constructor.
//! const LENGTH: usize = 1; //! const LENGTH: usize = 2;
//! //!
//! // This is what is called when we do `new Animal()` to construct the inner data of the class. //! // 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<Self> { //! // `_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<Self> {
//! // This is equivalent to `String(arg)`. //! // This is equivalent to `String(arg)`.
//! let kind = args.get_or_undefined(0).to_string(context)?; //! let kind = args.get_or_undefined(0).to_string(context)?;
//! //!
@ -41,7 +46,22 @@
//! Ok(animal) //! 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<()> { //! fn init(class: &mut ClassBuilder) -> JsResult<()> {
//! class.method( //! class.method(
//! js_string!("speak"), //! js_string!("speak"),
@ -49,19 +69,36 @@
//! NativeFunction::from_fn_ptr(|this, _args, _ctx| { //! NativeFunction::from_fn_ptr(|this, _args, _ctx| {
//! if let Some(object) = this.as_object() { //! if let Some(object) = this.as_object() {
//! if let Some(animal) = object.downcast_ref::<Animal>() { //! if let Some(animal) = object.downcast_ref::<Animal>() {
//! match &*animal { //! return Ok(match &*animal {
//! Self::Cat => println!("meow"), //! Self::Cat => js_string!("meow"),
//! Self::Dog => println!("woof"), //! Self::Dog => js_string!("woof"),
//! Self::Other => println!(r"¯\_(ツ)_/¯"), //! Self::Other => js_string!(r"¯\_(ツ)_/¯"),
//! }.into());
//! } //! }
//! } //! }
//! } //! Err(JsNativeError::typ().with_message("invalid this for class method").into())
//! Ok(JsValue::undefined())
//! }), //! }),
//! ); //! );
//! Ok(()) //! Ok(())
//! } //! }
//! } //! }
//!
//! fn main() {
//! let mut context = Context::default();
//!
//! context.register_global_class::<Animal>().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 //! [class-trait]: ./trait.Class.html
@ -79,6 +116,8 @@ use crate::{
}; };
/// Native class. /// Native class.
///
/// See the [module-level documentation][self] for more details.
pub trait Class: NativeObject + Sized { pub trait Class: NativeObject + Sized {
/// The binding name of this class. /// The binding name of this class.
const NAME: &'static str; const NAME: &'static str;
@ -88,25 +127,42 @@ pub trait Class: NativeObject + Sized {
/// Default is `writable`, `enumerable`, `configurable`. /// Default is `writable`, `enumerable`, `configurable`.
const ATTRIBUTES: Attribute = Attribute::all(); 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<Self>;
/// Initializes the properties and methods of this class. /// Initializes the properties and methods of this class.
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()>; 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<Self>;
/// Initializes the properties of the constructed object for an instance of this class.
///
/// 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`].
/// ///
/// # Note /// # Errors
/// ///
/// This will throw an error if this class is not registered in the context's active realm. /// - 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`]. /// See [`Context::register_global_class`].
/// ///
/// # Warning /// <div class="warning">
///
/// Overriding this method could be useful for certain usages, but incorrectly implementing this /// 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. /// could lead to weird errors like missing inherited methods or incorrect internal data.
/// </div>
fn construct( fn construct(
new_target: &JsValue, new_target: &JsValue,
args: &[JsValue], args: &[JsValue],
@ -121,30 +177,72 @@ pub trait Class: NativeObject + Sized {
.into()); .into());
} }
let class = context.get_global_class::<Self>().ok_or_else(|| { 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::<Self>()
.ok_or_else(|| {
JsNativeError::typ().with_message(format!( JsNativeError::typ().with_message(format!(
"could not find native class `{}` in the map of registered classes", "could not find native class `{}` in the map of registered classes",
Self::NAME Self::NAME
)) ))
})?; })?
.prototype()
};
let prototype = new_target let data = Self::data_constructor(new_target, args, context)?;
.as_object()
.map(|obj| {
obj.get(PROTOTYPE, context)
.map(|val| val.as_object().cloned())
})
.transpose()?
.flatten()
.unwrap_or_else(|| class.prototype());
let data = Self::make_data(new_target, args, context)?; let object = JsObject::from_proto_and_data_with_shared_shape(
let instance = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(), context.root_shape(),
prototype, prototype,
ObjectData::native_object(data), ObjectData::native_object(data),
); );
Ok(instance)
Self::object_constructor(&object, args, context)?;
Ok(object)
}
/// 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`].
///
/// <div class="warning">
/// 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.
/// </div>
fn from_data(data: Self, context: &mut Context) -> JsResult<JsObject> {
let prototype = context
.get_global_class::<Self>()
.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),
);
Self::object_constructor(&object, &[], context)?;
Ok(object)
} }
} }

3
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")"#)); /// let result = context.eval(Source::from_bytes(r#"eval("let a = 5")"#));
/// assert_eq!( /// assert_eq!(
/// result.unwrap_err().to_string(), /// result.unwrap_err().to_string(),

4
boa_engine/src/job.rs

@ -252,14 +252,14 @@ pub trait JobQueue {
/// can be done by passing it to the [`ContextBuilder`]: /// can be done by passing it to the [`ContextBuilder`]:
/// ///
/// ``` /// ```
/// use std::rc::Rc;
/// use boa_engine::{ /// use boa_engine::{
/// context::ContextBuilder, /// context::ContextBuilder,
/// job::{IdleJobQueue, JobQueue}, /// job::{IdleJobQueue, JobQueue},
/// }; /// };
/// use std::rc::Rc;
/// ///
/// let queue = Rc::new(IdleJobQueue); /// 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 /// [`ContextBuilder`]: crate::context::ContextBuilder

16
boa_engine/src/object/mod.rs

@ -2645,7 +2645,7 @@ impl<'ctx> ObjectInitializer<'ctx> {
} }
/// Create a new `ObjectBuilder` with custom [`NativeObject`] data. /// Create a new `ObjectBuilder` with custom [`NativeObject`] data.
pub fn with_native<T: NativeObject>(data: T, context: &'ctx mut Context) -> Self { pub fn with_native_data<T: NativeObject>(data: T, context: &'ctx mut Context) -> Self {
let object = JsObject::from_proto_and_data_with_shared_shape( let object = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(), context.root_shape(),
context.intrinsics().constructors().object().prototype(), context.intrinsics().constructors().object().prototype(),
@ -2654,6 +2654,20 @@ impl<'ctx> ObjectInitializer<'ctx> {
Self { context, object } Self { context, object }
} }
/// Create a new `ObjectBuilder` with custom [`NativeObject`] data and custom prototype.
pub fn with_native_data_and_proto<T: NativeObject>(
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. /// Add a function to the object.
pub fn function<B>(&mut self, function: NativeFunction, binding: B, length: usize) -> &mut Self pub fn function<B>(&mut self, function: NativeFunction, binding: B, length: usize) -> &mut Self
where where

6
boa_examples/src/bin/classes.rs

@ -65,7 +65,11 @@ impl Class for Person {
const LENGTH: usize = 2; const LENGTH: usize = 2;
// This is what is internally called when we construct a `Person` with the expression `new Person()`. // 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<Self> { fn data_constructor(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<Self> {
// We get the first argument. If it is unavailable we default to `undefined`, // We get the first argument. If it is unavailable we default to `undefined`,
// and then we call `to_string()`. // and then we call `to_string()`.
// //

2
boa_runtime/src/console/mod.rs

@ -163,7 +163,7 @@ impl Console {
let state = Rc::new(RefCell::new(Self::default())); let state = Rc::new(RefCell::new(Self::default()));
ObjectInitializer::with_native(Self::default(), context) ObjectInitializer::with_native_data(Self::default(), context)
.function( .function(
console_method(Self::assert, state.clone()), console_method(Self::assert, state.clone()),
js_string!("assert"), js_string!("assert"),

Loading…
Cancel
Save