Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TryFromJs for TypedJsFunction and more tests #3981

Merged
merged 9 commits into from
Sep 22, 2024
36 changes: 36 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,12 @@ impl<A: TryIntoJsArguments, R: TryFromJs> TypedJsFunction<A, R> {
self.inner.clone()
}

/// Get the inner `JsFunction` without consuming this object.
#[must_use]
pub fn as_js_function(&self) -> &JsFunction {
&self.inner
}

/// Call the function with the given arguments.
#[inline]
pub fn call(&self, context: &mut Context, args: A) -> JsResult<R> {
Expand All @@ -69,6 +75,36 @@ 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()
})
.map(JsFunction::typed),
_ => Err(JsNativeError::typ()
.with_message("value is not a Function object")
.into()),
}
}
}

impl<A: TryIntoJsArguments, R: TryFromJs> From<TypedJsFunction<A, R>> for JsValue {
#[inline]
fn from(o: TypedJsFunction<A, R>) -> Self {
o.into_inner().into()
}
}

impl<A: TryIntoJsArguments, R: TryFromJs> From<TypedJsFunction<A, R>> for JsFunction {
fn from(value: TypedJsFunction<A, R>) -> Self {
value.inner.clone()
}
}

/// 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
19 changes: 19 additions & 0 deletions core/interop/tests/assets/fibonacci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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);
}
97 changes: 97 additions & 0 deletions core/interop/tests/fibonacci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#![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;

#[allow(clippy::needless_pass_by_value)]
fn fibonacci(
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 {
Ok(
cb_a.call(context, (a - 1, cb_b.clone().into(), cb_a.clone().into()))?
+ cb_b.call(context, (a - 2, cb_b.clone().into(), cb_a.clone().into()))?,
)
}
}

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(a, cb_a, cb_b, context)
}
}

#[test]
fn fibonacci_test() {
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_rust = fibonacci
.into_js_function_copied(context)
.to_js_function(context.realm());

assert_eq!(
fibonacci_js
.call(
context,
(
10,
fibonacci_rust.clone(),
fibonacci_js.as_js_function().clone()
)
)
.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.as_js_function().clone()
)
)
.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);
}
Loading