Skip to content

Commit

Permalink
Add TryFromJs for TypedJsFunction and more tests (#3981)
Browse files Browse the repository at this point in the history
* Add TryFromJs for TypedJsFunction and more tests

Includes adding TryFromJs for "()".

* Fix clippies and fmt

* Prettier

* Add From<TypedJsFunction> for JsValue to allow conversion

* Implement comments

* clippies
  • Loading branch information
hansl authored Sep 22, 2024
1 parent ddb1901 commit 8438ad2
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 1 deletion.
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);
}

0 comments on commit 8438ad2

Please sign in to comment.