Skip to content

Commit

Permalink
Add more utility traits and funtions to boa_interop
Browse files Browse the repository at this point in the history
  • Loading branch information
hansl committed Mar 29, 2024
1 parent 79ea902 commit 90ec36f
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 27 deletions.
91 changes: 91 additions & 0 deletions core/interop/src/into_js_function_impls.rs
Original file line number Diff line number Diff line change
@@ -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<JsValue>,
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<JsValue>,
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<R, T> IntoJsFunction<(), R> for T
where
R: Into<JsValue>,
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);
212 changes: 185 additions & 27 deletions core/interop/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -35,29 +35,152 @@ impl<T: IntoIterator<Item = (JsString, NativeFunction)> + 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<RefCell>` 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<A, R> {
/// Converts the type into a JS function.
fn into_js_function(self, context: &mut Context) -> NativeFunction;
}

impl<T: FnMut() + 'static> 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<T: TryFromJs> 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<JsValue>);

#[allow(unused)]
impl JsRest {
/// Consumes the `JsRest` and returns the inner list of `JsValue`.
fn into_inner(self) -> Vec<JsValue> {
self.0
}

/// Returns an iterator over the inner list of `JsValue`.
fn iter(&self) -> impl Iterator<Item = &JsValue> {
self.0.iter()
}

/// Returns a mutable iterator over the inner list of `JsValue`.
fn iter_mut(&mut self) -> impl Iterator<Item = &mut JsValue> {
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<JsValue>;

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<T: TryFromJs>(pub T);

impl<T: TryFromJs> TryFromJsArgument for JsThis<T> {
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());
Expand All @@ -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::<i32>(context).unwrap())
.sum::<i32>();
}
}
.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,
Expand All @@ -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();
Expand All @@ -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));
}

0 comments on commit 90ec36f

Please sign in to comment.