diff --git a/core/interop/src/into_js_function_impls.rs b/core/interop/src/into_js_function_impls.rs new file mode 100644 index 00000000000..90b1e2e72df --- /dev/null +++ b/core/interop/src/into_js_function_impls.rs @@ -0,0 +1,91 @@ +//! Implementations of the `IntoJsFunction` trait for various function signatures. + +use crate::{IntoJsFunction, TryFromJsArgument}; +use boa_engine::{Context, JsValue, NativeFunction}; +use std::cell::RefCell; + +/// A token to represent the context argument in the function signature. +/// This should not be used directly and has no external meaning. +#[derive(Debug, Copy, Clone)] +pub struct ContextArgToken(); + +macro_rules! impl_into_js_function { + ($($id: ident: $t: ident),*) => { + impl<$($t,)* R, T> IntoJsFunction<($($t,)*), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: Into, + T: FnMut($($t,)*) -> R + 'static, + { + #[allow(unused_variables)] + fn into_js_function(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s.borrow_mut()( $($id),* ); + Ok(r.into()) + }) + } + } + } + + impl<$($t,)* R, T> IntoJsFunction<($($t,)* ContextArgToken), R> for T + where + $($t: TryFromJsArgument + 'static,)* + R: Into, + T: FnMut($($t,)* &mut Context) -> R + 'static, + { + #[allow(unused_variables)] + fn into_js_function(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |this, args, ctx| { + let rest = args; + $( + let ($id, rest) = $t::try_from_js_argument(this, rest, ctx)?; + )* + let r = s.borrow_mut()( $($id,)* ctx); + Ok(r.into()) + }) + } + } + } + }; +} + +impl IntoJsFunction<(), R> for T +where + R: Into, + T: FnMut() -> R + 'static, +{ + fn into_js_function(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + unsafe { + NativeFunction::from_closure(move |_this, _args, _ctx| { + let r = s.borrow_mut()(); + Ok(r.into()) + }) + } + } +} + +// Currently implemented up to 12 arguments. The empty argument list +// is implemented separately above. +// Consider that JsRest and JsThis are part of this list, but Context +// is not, as it is a special specialization of the template. +impl_into_js_function!(a: A); +impl_into_js_function!(a: A, b: B); +impl_into_js_function!(a: A, b: B, c: C); +impl_into_js_function!(a: A, b: B, c: C, d: D); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K); +impl_into_js_function!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L); diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index fc073f3f1f1..a2b380e59b0 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -1,8 +1,8 @@ //! Interop utilities between Boa and its host. -use std::cell::RefCell; use boa_engine::module::SyntheticModuleInitializer; -use boa_engine::{Context, JsString, JsValue, Module, NativeFunction}; +use boa_engine::value::TryFromJs; +use boa_engine::{Context, JsResult, JsString, JsValue, Module, NativeFunction}; pub mod loaders; @@ -35,29 +35,152 @@ impl + Clone> IntoJsModule fo } /// A trait to convert a type into a JS function. -pub trait IntoJsFunction { +/// This trait is implemented for functions with various signatures. +/// +/// For example: +/// ``` +/// # use boa_engine::{Context, JsValue, NativeFunction}; +/// # use boa_interop::IntoJsFunction; +/// # let mut context = Context::default(); +/// let f = |a: i32, b: i32| a + b; +/// let f = f.into_js_function(&mut context); +/// let result = f.call(&JsValue::undefined(), &[JsValue::from(1), JsValue::from(2)], &mut context).unwrap(); +/// assert_eq!(result, JsValue::new(3)); +/// ``` +/// +/// Since the `IntoJsFunction` trait is implemented for `FnMut`, you can +/// also use closures directly: +/// ``` +/// # use boa_engine::{Context, JsValue, NativeFunction}; +/// # use boa_interop::IntoJsFunction; +/// # use std::cell::RefCell; +/// # use std::rc::Rc; +/// # let mut context = Context::default(); +/// let mut x = Rc::new(RefCell::new(0)); +/// // Because NativeFunction takes ownership of the closure, +/// // the compiler cannot be certain it won't outlive `x`, so +/// // we need to create a `Rc` and share it. +/// let f = { +/// let x = x.clone(); +/// move |a: i32| *x.borrow_mut() += a +/// }; +/// let f = f.into_js_function(&mut context); +/// f.call(&JsValue::undefined(), &[JsValue::from(1)], &mut context).unwrap(); +/// f.call(&JsValue::undefined(), &[JsValue::from(4)], &mut context).unwrap(); +/// assert_eq!(*x.borrow(), 5); +/// ``` +pub trait IntoJsFunction { /// Converts the type into a JS function. fn into_js_function(self, context: &mut Context) -> NativeFunction; } -impl IntoJsFunction for T { - fn into_js_function(self, _context: &mut Context) -> NativeFunction { - let s = RefCell::new(self); +/// Create a Rust value from a JS argument. This trait is used to +/// convert arguments from JS to Rust types. It allows support +/// for optional arguments or rest arguments. +pub trait TryFromJsArgument: Sized { + /// Try to convert a JS argument into a Rust value, returning the + /// value and the rest of the arguments to be parsed. + /// + /// # Errors + /// Any parsing errors that may occur during the conversion. + fn try_from_js_argument<'a>( + this: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])>; +} - unsafe { - NativeFunction::from_closure(move |_, _, _| { - s.borrow_mut()(); - Ok(JsValue::undefined()) - }) +impl TryFromJsArgument for T { + fn try_from_js_argument<'a>( + _: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + match rest.split_first() { + Some((first, rest)) => Ok((first.try_js_into(context)?, rest)), + None => T::try_from_js(&JsValue::undefined(), context).map(|v| (v, rest)), } } } +/// An argument that when used in a JS function will empty the list +/// of JS arguments as JsValues. This can be used for having the +/// rest of the arguments in a function. +#[derive(Debug, Clone)] +pub struct JsRest(pub Vec); + +#[allow(unused)] +impl JsRest { + /// Consumes the `JsRest` and returns the inner list of `JsValue`. + fn into_inner(self) -> Vec { + self.0 + } + + /// Returns an iterator over the inner list of `JsValue`. + fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Returns a mutable iterator over the inner list of `JsValue`. + fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut() + } + + /// Returns the length of the inner list of `JsValue`. + fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the inner list of `JsValue` is empty. + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl IntoIterator for JsRest { + type Item = JsValue; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.into_inner().into_iter() + } +} + +impl TryFromJsArgument for JsRest { + fn try_from_js_argument<'a>( + _: &'a JsValue, + rest: &'a [JsValue], + _context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + Ok((JsRest(rest.to_vec()), &[])) + } +} + +/// Captures the `this` value in a JS function. Although this can be +/// specified multiple times as argument, it will always be filled +/// with clone of the same value. +#[derive(Debug, Clone)] +pub struct JsThis(pub T); + +impl TryFromJsArgument for JsThis { + fn try_from_js_argument<'a>( + this: &'a JsValue, + rest: &'a [JsValue], + context: &mut Context, + ) -> JsResult<(Self, &'a [JsValue])> { + Ok((JsThis(this.try_js_into(context)?), rest)) + } +} + +// Implement `IntoJsFunction` for functions with a various list of +// arguments. +mod into_js_function_impls; + #[test] #[allow(clippy::missing_panics_doc)] pub fn into_js_module() { - use boa_engine::builtins::promise::PromiseState; use boa_engine::{js_string, JsValue, Source}; + use std::cell::RefCell; use std::rc::Rc; let loader = Rc::new(loaders::HashMapModuleLoader::new()); @@ -68,26 +191,53 @@ pub fn into_js_module() { let foo_count = Rc::new(RefCell::new(0)); let bar_count = Rc::new(RefCell::new(0)); + let dad_count = Rc::new(RefCell::new(0)); + let result = Rc::new(RefCell::new(JsValue::undefined())); let module = vec![ ( js_string!("foo"), + { + let counter = foo_count.clone(); + move || { + *counter.borrow_mut() += 1; + let result = *counter.borrow(); + result + } + } + .into_js_function(&mut context), + ), + ( + js_string!("bar"), IntoJsFunction::into_js_function( { - let foo_count = foo_count.clone(); - move || { - *foo_count.borrow_mut() += 1; + let counter = bar_count.clone(); + move |i: i32| { + *counter.borrow_mut() += i; } }, &mut context, ), ), ( - js_string!("bar"), + js_string!("dad"), + { + let counter = dad_count.clone(); + move |args: JsRest, context: &mut Context| { + *counter.borrow_mut() += args + .into_iter() + .map(|i| i.try_js_into::(context).unwrap()) + .sum::(); + } + } + .into_js_function(&mut context), + ), + ( + js_string!("send"), IntoJsFunction::into_js_function( { - let bar_count = bar_count.clone(); - move || { - *bar_count.borrow_mut() += 1; + let result = result.clone(); + move |value: JsValue| { + *result.borrow_mut() = value; } }, &mut context, @@ -102,11 +252,15 @@ pub fn into_js_module() { r" import * as test from 'test'; let result = test.foo(); - for (let i = 0; i < 10; i++) { - test.bar(); + test.foo(); + for (let i = 1; i <= 5; i++) { + test.bar(i); + } + for (let i = 1; i < 5; i++) { + test.dad(1, 2, 3); } - result + test.send(result); ", ); let root_module = Module::parse(source, None, &mut context).unwrap(); @@ -115,11 +269,15 @@ pub fn into_js_module() { context.run_jobs(); // Checking if the final promise didn't return an error. - let PromiseState::Fulfilled(v) = promise_result.state() else { - panic!("module didn't execute successfully!") + if promise_result.state().as_fulfilled().is_none() { + panic!( + "module didn't execute successfully! Promise: {:?}", + promise_result.state() + ); }; - assert_eq!(*foo_count.borrow(), 1); - assert_eq!(*bar_count.borrow(), 10); - assert_eq!(v, JsValue::undefined()); + assert_eq!(*foo_count.borrow(), 2); + assert_eq!(*bar_count.borrow(), 15); + assert_eq!(*dad_count.borrow(), 24); + assert_eq!(result.borrow().clone().try_js_into(&mut context), Ok(1u32)); }