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. 2
      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.
//!
//! 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<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)`.
//! 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::<Animal>() {
//! 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::<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
@ -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<Self>;
/// 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<Self>;
/// 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`].
///
/// <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 construct(
new_target: &JsValue,
args: &[JsValue],
@ -121,30 +177,72 @@ pub trait Class: NativeObject + Sized {
.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!(
"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`].
///
/// <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),
);
Ok(instance)
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")"#));
/// assert_eq!(
/// result.unwrap_err().to_string(),

2
boa_engine/src/job.rs

@ -252,11 +252,11 @@ 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();

16
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<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(
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<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.
pub fn function<B>(&mut self, function: NativeFunction, binding: B, length: usize) -> &mut Self
where

6
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<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`,
// 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()));
ObjectInitializer::with_native(Self::default(), context)
ObjectInitializer::with_native_data(Self::default(), context)
.function(
console_method(Self::assert, state.clone()),
js_string!("assert"),

Loading…
Cancel
Save