Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility methods to the Class trait #3488

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
178 changes: 138 additions & 40 deletions boa_engine/src/class.rs
Original file line number Diff line number Diff line change
@@ -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};
//! #
Expand All @@ -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)?;
//!
Expand All @@ -41,27 +46,59 @@
//! 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"),
//! 0,
//! 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
Expand All @@ -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;
Expand All @@ -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],
Expand All @@ -121,30 +177,72 @@ pub trait Class: NativeObject + Sized {
.into());
}

let class = 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
))
})?;
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)
}
}

Expand Down
3 changes: 2 additions & 1 deletion boa_engine/src/context/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions boa_engine/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion boa_engine/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion boa_examples/src/bin/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
//
Expand Down
2 changes: 1 addition & 1 deletion boa_runtime/src/console/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down