mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
12 KiB
378 lines
12 KiB
//! 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, Source, JsObject, js_string, |
|
//! # JsNativeError, JsData, |
|
//! # }; |
|
//! # use boa_gc::{Finalize, Trace}; |
|
//! # |
|
//! // Can also be a struct containing `Trace` types. |
|
//! #[derive(Debug, Trace, Finalize, JsData)] |
|
//! enum Animal { |
|
//! Cat, |
|
//! Dog, |
|
//! Other, |
|
//! } |
|
//! |
|
//! impl Class for Animal { |
|
//! // we set the binging name of this function to be `"Animal"`. |
|
//! const NAME: &'static str = "Animal"; |
|
//! |
|
//! // 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. |
|
//! // `_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)?; |
|
//! |
|
//! let animal = match kind.to_std_string_escaped().as_str() { |
|
//! "cat" => Self::Cat, |
|
//! "dog" => Self::Dog, |
|
//! _ => Self::Other, |
|
//! }; |
|
//! |
|
//! Ok(animal) |
|
//! } |
|
//! |
|
//! // 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>() { |
|
//! return Ok(match &*animal { |
|
//! Self::Cat => js_string!("meow"), |
|
//! Self::Dog => js_string!("woof"), |
|
//! Self::Other => js_string!(r"¯\_(ツ)_/¯"), |
|
//! }.into()); |
|
//! } |
|
//! } |
|
//! 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 |
|
|
|
use crate::{ |
|
context::intrinsics::StandardConstructor, |
|
error::JsNativeError, |
|
native_function::NativeFunction, |
|
object::{ConstructorBuilder, FunctionBinding, JsFunction, JsObject, NativeObject, PROTOTYPE}, |
|
property::{Attribute, PropertyDescriptor, PropertyKey}, |
|
Context, JsResult, JsValue, |
|
}; |
|
|
|
/// 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; |
|
/// The amount of arguments this class' constructor takes. Default is `0`. |
|
const LENGTH: usize = 0; |
|
/// The property attributes of this class' constructor in the global object. |
|
/// Default is `writable`, `enumerable`, `configurable`. |
|
const ATTRIBUTES: Attribute = Attribute::all(); |
|
|
|
/// Initializes the properties and methods of this class. |
|
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()>; |
|
|
|
/// 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`]. |
|
/// |
|
/// # Errors |
|
/// |
|
/// - 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], |
|
context: &mut Context, |
|
) -> JsResult<JsObject> { |
|
if new_target.is_undefined() { |
|
return Err(JsNativeError::typ() |
|
.with_message(format!( |
|
"cannot call constructor of native class `{}` without new", |
|
Self::NAME |
|
)) |
|
.into()); |
|
} |
|
|
|
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, data); |
|
|
|
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, data); |
|
|
|
Self::object_constructor(&object, &[], context)?; |
|
|
|
Ok(object) |
|
} |
|
} |
|
|
|
/// Class builder which allows adding methods and static methods to the class. |
|
#[derive(Debug)] |
|
pub struct ClassBuilder<'ctx> { |
|
builder: ConstructorBuilder<'ctx>, |
|
} |
|
|
|
impl<'ctx> ClassBuilder<'ctx> { |
|
pub(crate) fn new<T>(context: &'ctx mut Context) -> Self |
|
where |
|
T: Class, |
|
{ |
|
let mut builder = ConstructorBuilder::new( |
|
context, |
|
NativeFunction::from_fn_ptr(|t, a, c| T::construct(t, a, c).map(JsValue::from)), |
|
); |
|
builder.name(T::NAME); |
|
builder.length(T::LENGTH); |
|
Self { builder } |
|
} |
|
|
|
pub(crate) fn build(self) -> StandardConstructor { |
|
self.builder.build() |
|
} |
|
|
|
/// Add a method to the class. |
|
/// |
|
/// It is added to `prototype`. |
|
pub fn method<N>(&mut self, name: N, length: usize, function: NativeFunction) -> &mut Self |
|
where |
|
N: Into<FunctionBinding>, |
|
{ |
|
self.builder.method(function, name, length); |
|
self |
|
} |
|
|
|
/// Add a static method to the class. |
|
/// |
|
/// It is added to class object itself. |
|
pub fn static_method<N>( |
|
&mut self, |
|
name: N, |
|
length: usize, |
|
function: NativeFunction, |
|
) -> &mut Self |
|
where |
|
N: Into<FunctionBinding>, |
|
{ |
|
self.builder.static_method(function, name, length); |
|
self |
|
} |
|
|
|
/// Add a data property to the class, with the specified attribute. |
|
/// |
|
/// It is added to `prototype`. |
|
pub fn property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
V: Into<JsValue>, |
|
{ |
|
self.builder.property(key, value, attribute); |
|
self |
|
} |
|
|
|
/// Add a static data property to the class, with the specified attribute. |
|
/// |
|
/// It is added to class object itself. |
|
pub fn static_property<K, V>(&mut self, key: K, value: V, attribute: Attribute) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
V: Into<JsValue>, |
|
{ |
|
self.builder.static_property(key, value, attribute); |
|
self |
|
} |
|
|
|
/// Add an accessor property to the class, with the specified attribute. |
|
/// |
|
/// It is added to `prototype`. |
|
pub fn accessor<K>( |
|
&mut self, |
|
key: K, |
|
get: Option<JsFunction>, |
|
set: Option<JsFunction>, |
|
attribute: Attribute, |
|
) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
{ |
|
self.builder.accessor(key, get, set, attribute); |
|
self |
|
} |
|
|
|
/// Add a static accessor property to the class, with the specified attribute. |
|
/// |
|
/// It is added to class object itself. |
|
pub fn static_accessor<K>( |
|
&mut self, |
|
key: K, |
|
get: Option<JsFunction>, |
|
set: Option<JsFunction>, |
|
attribute: Attribute, |
|
) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
{ |
|
self.builder.static_accessor(key, get, set, attribute); |
|
self |
|
} |
|
|
|
/// Add a property descriptor to the class, with the specified attribute. |
|
/// |
|
/// It is added to `prototype`. |
|
pub fn property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
P: Into<PropertyDescriptor>, |
|
{ |
|
self.builder.property_descriptor(key, property); |
|
self |
|
} |
|
|
|
/// Add a static property descriptor to the class, with the specified attribute. |
|
/// |
|
/// It is added to class object itself. |
|
pub fn static_property_descriptor<K, P>(&mut self, key: K, property: P) -> &mut Self |
|
where |
|
K: Into<PropertyKey>, |
|
P: Into<PropertyDescriptor>, |
|
{ |
|
self.builder.static_property_descriptor(key, property); |
|
self |
|
} |
|
|
|
/// Return the current context. |
|
#[inline] |
|
pub fn context(&mut self) -> &mut Context { |
|
self.builder.context() |
|
} |
|
}
|
|
|