Skip to content

Commit

Permalink
Add TryFromJs for TypedJsFunction and more tests
Browse files Browse the repository at this point in the history
Includes adding TryFromJs for "()".
  • Loading branch information
hansl committed Sep 5, 2024
1 parent eecba59 commit e665b35
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 1 deletion.
22 changes: 22 additions & 0 deletions core/engine/src/object/builtins/jsfunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ impl<A: TryIntoJsArguments, R: TryFromJs> TypedJsFunction<A, R> {
self.inner.clone()
}

/// Get the inner `JsFunction` without consuming this object.
pub fn to_js_function(&self) -> JsFunction {
self.inner.clone()
}

/// Call the function with the given arguments.
#[inline]
pub fn call(&self, context: &mut Context, args: A) -> JsResult<R> {
Expand All @@ -69,6 +74,23 @@ impl<A: TryIntoJsArguments, R: TryFromJs> TypedJsFunction<A, R> {
}
}

impl<A: TryIntoJsArguments, R: TryFromJs> TryFromJs for TypedJsFunction<A, R> {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Object(o) => JsFunction::from_object(o.clone())
.ok_or_else(|| {
JsNativeError::typ()
.with_message("object is not a function")
.into()
})
.and_then(|f| Ok(f.typed())),
_ => Err(JsNativeError::typ()
.with_message("value is not a Function object")
.into()),
}
}
}

/// JavaScript `Function` rust object.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct JsFunction {
Expand Down
6 changes: 6 additions & 0 deletions core/engine/src/value/conversions/try_from_js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ impl TryFromJs for bool {
}
}

impl TryFromJs for () {
fn try_from_js(_value: &JsValue, _context: &mut Context) -> JsResult<Self> {
Ok(())
}
}

impl TryFromJs for String {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {
Expand Down
3 changes: 2 additions & 1 deletion core/interop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ impl<'a, T: TryFromJs> TryFromJsArgument<'a> for T {
}
}

/// An argument that would be ignored in a JS function.
/// An argument that would be ignored in a JS function. This is equivalent of typing
/// `()` in Rust functions argument, but more explicit.
#[derive(Debug, Clone, Copy)]
pub struct Ignore;

Expand Down
16 changes: 16 additions & 0 deletions core/interop/tests/assets/fibonacci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Calculate a fibonacci number by calling callbacks with intermediate results,
* switching between Rust and JavaScript.
* @param {number} a The fibonacci number to calculate.
* @param {function} callback_a A callback method.
* @param {function} callback_b A callback method.
* @returns {number} The {a}th fibonacci number.
*/
export function fibonacci(a, callback_a, callback_b) {
if (a <= 1) {
return a;
}

// Switch the callbacks around.
return callback_a(a - 1, callback_b, callback_a) + callback_b(a - 2, callback_b, callback_a);
}
28 changes: 28 additions & 0 deletions core/interop/tests/assets/gcd_callback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Calculate the greatest common divisor of two numbers.
* @param {number} a
* @param {number} b
* @param {function} callback A callback method to call with the result.
* @returns {number|*} The greatest common divisor of {a} and {b}.
* @throws {TypeError} If either {a} or {b} is not finite.
*/
export function gcd_callback(a, b, callback) {
a = +a;
b = +b;
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new TypeError("Invalid input");
}

// Euclidean algorithm
function inner_gcd(a, b) {
while (b !== 0) {
let t = b;
b = a % b;
a = t;
}
return a;
}

let result = inner_gcd(a, b);
callback(result);
}
91 changes: 91 additions & 0 deletions core/interop/tests/fibonacci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#![allow(unused_crate_dependencies)]
//! A test that goes back and forth between JavaScript and Rust.

// You can execute this example with `cargo run --example gcd`

use boa_engine::object::builtins::{JsFunction, TypedJsFunction};
use boa_engine::{js_error, js_str, Context, JsResult, Module, Source};
use boa_interop::IntoJsFunctionCopied;
use std::path::PathBuf;

fn fibonacci_rs(
a: usize,
cb_a: TypedJsFunction<(usize, JsFunction, JsFunction), usize>,
cb_b: TypedJsFunction<(usize, JsFunction, JsFunction), usize>,
context: &mut Context,
) -> JsResult<usize> {
if a <= 1 {
Ok(a)
} else {
let cb_a1 = cb_a.to_js_function();
let cb_b1 = cb_b.to_js_function();
let cb_a2 = cb_a1.clone();
let cb_b2 = cb_b1.clone();

Ok(cb_a.call(context, (a - 1, cb_b1, cb_a1))?
+ cb_b.call(context, (a - 2, cb_b2, cb_a2))?)
}
}

fn fibonacci_throw(
a: usize,
cb_a: TypedJsFunction<(usize, JsFunction, JsFunction), usize>,
cb_b: TypedJsFunction<(usize, JsFunction, JsFunction), usize>,
context: &mut Context,
) -> JsResult<usize> {
if a < 5 {
Err(js_error!("a is too small"))
} else {
fibonacci_rs(a, cb_a, cb_b, context)
}
}

#[test]
fn fibonacci() {
let assets_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("tests/assets");

// Create the engine.
let context = &mut Context::default();

// Load the JavaScript code.
let gcd_path = assets_dir.join("fibonacci.js");
let source = Source::from_filepath(&gcd_path).unwrap();
let module = Module::parse(source, None, context).unwrap();
module
.load_link_evaluate(context)
.await_blocking(context)
.unwrap();

let fibonacci_js = module
.get_typed_fn::<(usize, JsFunction, JsFunction), usize>(js_str!("fibonacci"), context)
.unwrap();

let fibonacci_rs = fibonacci_rs
.into_js_function_copied(context)
.to_js_function(context.realm());

assert_eq!(
fibonacci_js
.call(
context,
(10, fibonacci_rs.clone(), fibonacci_js.to_js_function())
)
.unwrap(),
55
);

let fibonacci_throw = fibonacci_throw
.into_js_function_copied(context)
.to_js_function(context.realm());
assert_eq!(
fibonacci_js
.call(
context,
(10, fibonacci_throw.clone(), fibonacci_js.to_js_function())
)
.unwrap_err()
.to_string(),
"\"a is too small\""
);
}
49 changes: 49 additions & 0 deletions core/interop/tests/gcd_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#![allow(unused_crate_dependencies)]
//! A test that mimics the boa_engine's GCD test with a typed callback.

use boa_engine::object::builtins::JsFunction;
use boa_engine::{js_str, Context, Module, Source};
use boa_gc::Gc;
use boa_interop::{ContextData, IntoJsFunctionCopied};
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};

fn callback_from_js(ContextData(r): ContextData<Gc<AtomicUsize>>, result: usize) {
r.store(result, Ordering::Relaxed);
}

#[test]
fn gcd_callback() {
let assets_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("tests/assets");

// Create the engine.
let context = &mut Context::default();
let result = Gc::new(AtomicUsize::new(0));
context.insert_data(result.clone());

// Load the JavaScript code.
let gcd_path = assets_dir.join("gcd_callback.js");
let source = Source::from_filepath(&gcd_path).unwrap();
let module = Module::parse(source, None, context).unwrap();
module
.load_link_evaluate(context)
.await_blocking(context)
.unwrap();

let js_gcd = module
.get_typed_fn::<(i32, i32, JsFunction), ()>(js_str!("gcd_callback"), context)
.unwrap();

let function = callback_from_js
.into_js_function_copied(context)
.to_js_function(context.realm());

result.store(0, Ordering::Relaxed);
assert_eq!(js_gcd.call(context, (6, 9, function.clone())), Ok(()));
assert_eq!(result.load(Ordering::Relaxed), 3);

result.store(0, Ordering::Relaxed);
assert_eq!(js_gcd.call(context, (9, 6, function)), Ok(()));
assert_eq!(result.load(Ordering::Relaxed), 3);
}

0 comments on commit e665b35

Please sign in to comment.