From 8438ad21cd0f6a99f1539e7cd4538336009b1f48 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Sat, 21 Sep 2024 17:35:30 -0700 Subject: [PATCH] Add TryFromJs for TypedJsFunction and more tests (#3981) * Add TryFromJs for TypedJsFunction and more tests Includes adding TryFromJs for "()". * Fix clippies and fmt * Prettier * Add From for JsValue to allow conversion * Implement comments * clippies --- core/engine/src/object/builtins/jsfunction.rs | 36 +++++++ .../src/value/conversions/try_from_js.rs | 6 ++ core/interop/src/lib.rs | 3 +- core/interop/tests/assets/fibonacci.js | 19 ++++ core/interop/tests/assets/gcd_callback.js | 28 ++++++ core/interop/tests/fibonacci.rs | 97 +++++++++++++++++++ core/interop/tests/gcd_callback.rs | 49 ++++++++++ 7 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 core/interop/tests/assets/fibonacci.js create mode 100644 core/interop/tests/assets/gcd_callback.js create mode 100644 core/interop/tests/fibonacci.rs create mode 100644 core/interop/tests/gcd_callback.rs diff --git a/core/engine/src/object/builtins/jsfunction.rs b/core/engine/src/object/builtins/jsfunction.rs index 4cc48dba02..16ff8fdd89 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -54,6 +54,12 @@ impl TypedJsFunction { 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 { @@ -69,6 +75,36 @@ impl TypedJsFunction { } } +impl TryFromJs for TypedJsFunction { + fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { + 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 From> for JsValue { + #[inline] + fn from(o: TypedJsFunction) -> Self { + o.into_inner().into() + } +} + +impl From> for JsFunction { + fn from(value: TypedJsFunction) -> Self { + value.inner.clone() + } +} + /// JavaScript `Function` rust object. #[derive(Debug, Clone, Trace, Finalize)] pub struct JsFunction { diff --git a/core/engine/src/value/conversions/try_from_js.rs b/core/engine/src/value/conversions/try_from_js.rs index b41645d0f3..ebe7c980fa 100644 --- a/core/engine/src/value/conversions/try_from_js.rs +++ b/core/engine/src/value/conversions/try_from_js.rs @@ -36,6 +36,12 @@ impl TryFromJs for bool { } } +impl TryFromJs for () { + fn try_from_js(_value: &JsValue, _context: &mut Context) -> JsResult { + Ok(()) + } +} + impl TryFromJs for String { fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { match value { diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index 2813e9d046..af282f77d3 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -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; diff --git a/core/interop/tests/assets/fibonacci.js b/core/interop/tests/assets/fibonacci.js new file mode 100644 index 0000000000..2e22645b76 --- /dev/null +++ b/core/interop/tests/assets/fibonacci.js @@ -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) + ); +} diff --git a/core/interop/tests/assets/gcd_callback.js b/core/interop/tests/assets/gcd_callback.js new file mode 100644 index 0000000000..b3a744ec3f --- /dev/null +++ b/core/interop/tests/assets/gcd_callback.js @@ -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); +} diff --git a/core/interop/tests/fibonacci.rs b/core/interop/tests/fibonacci.rs new file mode 100644 index 0000000000..0ce64e3cab --- /dev/null +++ b/core/interop/tests/fibonacci.rs @@ -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 { + 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 { + 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\"" + ); +} diff --git a/core/interop/tests/gcd_callback.rs b/core/interop/tests/gcd_callback.rs new file mode 100644 index 0000000000..359a79cbaf --- /dev/null +++ b/core/interop/tests/gcd_callback.rs @@ -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>, 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); +}