Browse Source

Add a js_class to implement the Class trait without boilerplate (#3872)

* Add a js_class to implement the Class trait without boilerplate

This also adds a JsInstance that verifies that `this` is of the proper
class, and an `Ignore` that ignore arguments.

The syntax is a bit special because of limitations of macro_rules. For
example, the way fields are defined. It was impossible to keep both
assignments (e.g. `public field = 123;`) and dynamic fields.

This reduces significantly the boilerplate for declaring classes. The
example in class.rs is more than twice as long (if you remove comments)
than the macro is.

* clippies

* Fix usage of macros

* Fix some imports issues

* Fix some imports issues, again

* Add a way to alias a function, e.g. change case

* Address comment and get rid of unwraps
pull/3906/head
Hans Larsen 4 months ago committed by GitHub
parent
commit
58d0fe64d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      core/engine/src/object/mod.rs
  2. 158
      core/interop/src/lib.rs
  3. 195
      core/interop/src/macros.rs

7
core/engine/src/object/mod.rs

@ -247,6 +247,13 @@ impl<T: ?Sized> Object<T> {
&self.data
}
/// Returns the data of the object.
#[inline]
#[must_use]
pub fn data_mut(&mut self) -> &mut T {
&mut self.data
}
/// Gets the prototype instance of this object.
#[inline]
#[must_use]

158
core/interop/src/lib.rs

@ -1,13 +1,19 @@
//! Interop utilities between Boa and its host.
use boa_engine::module::SyntheticModuleInitializer;
use boa_engine::object::Object;
use boa_engine::value::TryFromJs;
use boa_engine::{
Context, JsNativeError, JsResult, JsString, JsValue, Module, NativeFunction, NativeObject,
};
use std::ops::Deref;
pub use boa_engine;
use boa_gc::{GcRef, GcRefMut};
pub use boa_macros;
pub mod loaders;
pub mod macros;
/// Internal module only.
pub(crate) mod private {
@ -154,6 +160,20 @@ impl<'a, T: TryFromJs> TryFromJsArgument<'a> for T {
}
}
/// An argument that would be ignored in a JS function.
#[derive(Debug, Clone, Copy)]
pub struct Ignore;
impl<'a> TryFromJsArgument<'a> for Ignore {
fn try_from_js_argument(
_this: &'a JsValue,
rest: &'a [JsValue],
_: &mut Context,
) -> JsResult<(Self, &'a [JsValue])> {
Ok((Ignore, &rest[1..]))
}
}
/// An argument that when used in a JS function will empty the list
/// of JS arguments as `JsValue`s. This can be used for having the
/// rest of the arguments in a function. It should be the last
@ -327,6 +347,67 @@ impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsThis<T> {
}
}
impl<T: TryFromJs> Deref for JsThis<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Captures a class instance from the `this` value in a JS function. The class
/// will be a non-mutable reference of Rust type `T`, if it is an instance of `T`.
///
/// To have more flexibility on the parsing of the `this` value, you can use the
/// [`JsThis`] capture instead.
#[derive(Debug, Clone)]
pub struct JsClass<T: NativeObject> {
inner: boa_engine::JsObject<T>,
}
impl<T: NativeObject> JsClass<T> {
/// Borrow a reference to the class instance of type `T`.
///
/// # Panics
///
/// Panics if the object is currently borrowed.
///
/// This does not panic if the type is wrong, as the type is checked
/// during the construction of the `JsClass` instance.
#[must_use]
pub fn borrow(&self) -> GcRef<'_, T> {
GcRef::map(self.inner.borrow(), |obj| obj.data())
}
/// Borrow a mutable reference to the class instance of type `T`.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[must_use]
pub fn borrow_mut(&self) -> GcRefMut<'_, Object<T>, T> {
GcRefMut::map(self.inner.borrow_mut(), |obj| obj.data_mut())
}
}
impl<'a, T: NativeObject + 'static> TryFromJsArgument<'a> for JsClass<T> {
fn try_from_js_argument(
this: &'a JsValue,
rest: &'a [JsValue],
_context: &mut Context,
) -> JsResult<(Self, &'a [JsValue])> {
if let Some(object) = this.as_object() {
if let Ok(inner) = object.clone().downcast::<T>() {
return Ok((JsClass { inner }, rest));
}
}
Err(JsNativeError::typ()
.with_message("invalid this for class method")
.into())
}
}
/// Captures a [`ContextData`] data from the [`Context`] as a JS function argument,
/// based on its type.
///
@ -531,3 +612,80 @@ fn can_throw_exception() {
Some(&JsString::from("from javascript").into())
);
}
#[test]
fn class() {
use boa_engine::class::{Class, ClassBuilder};
use boa_engine::{js_string, JsValue, Source};
use boa_macros::{Finalize, JsData, Trace};
use std::rc::Rc;
#[derive(Debug, Trace, Finalize, JsData)]
struct Test {
value: i32,
}
impl Test {
#[allow(clippy::needless_pass_by_value)]
fn get_value(this: JsClass<Test>) -> i32 {
this.borrow().value
}
#[allow(clippy::needless_pass_by_value)]
fn set_value(this: JsClass<Test>, new_value: i32) {
(*this.borrow_mut()).value = new_value;
}
}
impl Class for Test {
const NAME: &'static str = "Test";
fn init(class: &mut ClassBuilder<'_>) -> JsResult<()> {
let get_value = Self::get_value.into_js_function_copied(class.context());
class.method(js_string!("getValue"), 0, get_value);
let set_value = Self::set_value.into_js_function_copied(class.context());
class.method(js_string!("setValue"), 1, set_value);
Ok(())
}
fn data_constructor(
_new_target: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<Self> {
Ok(Self { value: 123 })
}
}
let loader = Rc::new(loaders::HashMapModuleLoader::new());
let mut context = Context::builder()
.module_loader(loader.clone())
.build()
.unwrap();
context.register_global_class::<Test>().unwrap();
let source = Source::from_bytes(
r"
let t = new Test();
if (t.getValue() != 123) {
throw 'invalid value';
}
t.setValue(456);
if (t.getValue() != 456) {
throw 'invalid value 456';
}
",
);
let root_module = Module::parse(source, None, &mut context).unwrap();
let promise_result = root_module.load_link_evaluate(&mut context);
context.run_jobs();
// Checking if the final promise didn't return an error.
assert!(
promise_result.state().as_fulfilled().is_some(),
"module didn't execute successfully! Promise: {:?}",
promise_result.state()
);
}

195
core/interop/src/macros.rs

@ -0,0 +1,195 @@
//! Module declaring interop macro rules.
/// Declare a JavaScript class, in a simpler way.
///
/// This can make declaration of JavaScript classes easier by using an hybrid
/// declarative approach. The class itself follows a closer syntax to JavaScript
/// while the method arguments/results and bodies are written in Rust.
///
/// This only declares the Boa interop parts of the class. The actual type must
/// be declared separately as a Rust type, along with necessary derives and
/// traits.
///
/// Here's an example using the animal class declared in [`boa_engine::class`]:
/// # Example
/// ```
/// # use boa_engine::{JsString, JsData, js_string};
/// # use boa_gc::{Finalize, Trace};
/// use boa_interop::{js_class, Ignore, JsClass};
///
/// #[derive(Clone, Trace, Finalize, JsData)]
/// pub enum Animal {
/// Cat,
/// Dog,
/// Other,
/// }
///
/// js_class! {
/// // Implement [`Class`] trait for the `Animal` enum.
/// class Animal {
/// // This sets a field on the JavaScript object. The arguments to
/// // `init` are the arguments passed to the constructor. This
/// // function MUST return the value to be set on the field. If this
/// // returns a `JsResult`, it will be unwrapped and error out during
/// // construction of the object.
/// public age(_name: Ignore, age: i32) -> i32 {
/// age
/// }
///
/// // This is called when a new instance of the class is created in
/// // JavaScript, e.g. `new Animal("cat")`.
/// // This method is mandatory and MUST return `JsResult<Self>`.
/// constructor(name: String) {
/// match name.as_str() {
/// "cat" => Ok(Animal::Cat),
/// "dog" => Ok(Animal::Dog),
/// _ => Ok(Animal::Other),
/// }
/// }
///
/// // Declare a function on the class itself.
/// // There is a current limitation using `self` in methods, so the
/// // instance must be accessed using an actual argument.
/// fn speak(this: JsClass<Animal>) -> JsString {
/// match *this.borrow() {
/// Animal::Cat => js_string!("meow"),
/// Animal::Dog => js_string!("woof"),
/// Animal::Other => js_string!(r"¯\_(ツ)_/¯"),
/// }
/// }
/// }
/// }
///
/// fn main() {
///# use boa_engine::{Context, JsString, Source, js_str};
///
/// 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()}!`
/// "#)).expect("Could not evaluate script");
///
/// assert_eq!(
/// result.as_string().unwrap(),
/// &js_str!("My pet is 3 years old. Right, buddy? - woof!")
/// );
/// }
/// ```
#[macro_export]
macro_rules! js_class {
(
class $class_name: ident $(as $class_js_name: literal)? {
$(
$(#[$field_attr: meta])*
public $field_name: ident
( $( $field_arg: ident: $field_arg_type: ty ),* ) -> $field_ty: ty
$field_body: block
)*
$(#[$constructor_attr: meta])*
constructor( $( $ctor_arg: ident: $ctor_arg_ty: ty ),* )
$constructor_body: block
$(
$(#[$method_attr: meta])*
fn $method_name: ident $( as $method_js_name: literal )?
( $( $fn_arg: ident: $fn_arg_type: ty ),* )
$(-> $result_type: ty)?
$method_body: block
)*
}
) => {
impl $crate::boa_engine::class::Class for $class_name {
const NAME: &'static str = $crate::__js_class_name!($class_name, $($class_js_name)?);
const LENGTH: usize = $crate::__count!( $( $ctor_arg )* );
fn init(class: &mut $crate::boa_engine::class::ClassBuilder<'_>) -> $crate::boa_engine::JsResult<()> {
// Add all methods to the class.
$(
fn $method_name ( $($fn_arg: $fn_arg_type),* ) -> $( $result_type )?
$method_body
let function = $crate::IntoJsFunctionCopied::into_js_function_copied(
$method_name,
class.context(),
);
let function_name = $crate::__js_class_name!($method_name, $($method_js_name)?);
class.method(
$crate::boa_engine::JsString::from(function_name),
$crate::__count!($( $fn_arg )*),
function,
);
)*
Ok(())
}
fn data_constructor(
new_target: &$crate::boa_engine::JsValue,
args: &[$crate::boa_engine::JsValue],
context: &mut $crate::boa_engine::Context,
) -> $crate::boa_engine::JsResult<$class_name> {
let rest = args;
$(
let ($ctor_arg, rest) : ($ctor_arg_ty, _) = $crate::TryFromJsArgument::try_from_js_argument(new_target, rest, context)?;
)*
$constructor_body
}
fn object_constructor(
instance: &$crate::boa_engine::JsObject,
args: &[$crate::boa_engine::JsValue],
context: &mut $crate::boa_engine::Context
) -> $crate::boa_engine::JsResult<()> {
$(
fn $field_name ( $($field_arg: $field_arg_type),* ) -> $field_ty
$field_body
let function = $crate::IntoJsFunctionCopied::into_js_function_copied(
$field_name,
context,
);
instance.set(
$crate::boa_engine::JsString::from(stringify!($field_name)),
function.call(&$crate::boa_engine::JsValue::undefined(), args, context)?,
false,
context
);
)*
Ok(())
}
}
}
}
/// Internal macro to get the JavaScript class name.
#[macro_export]
macro_rules! __js_class_name {
($class_name: ident, $class_js_name: literal) => {
$class_js_name
};
($class_name: ident,) => {
stringify!($class_name)
};
}
/// Internal macro to get the JavaScript class length.
#[macro_export]
macro_rules! __count {
() => (0);
($_: ident $($rest: ident)*) => {
1 + $crate::__count!($($rest)*)
};
}
Loading…
Cancel
Save