From e665b35cb3b877fa377174db38231818e627b09c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 5 Sep 2024 11:59:42 -0700 Subject: [PATCH 1/6] Add TryFromJs for TypedJsFunction and more tests Includes adding TryFromJs for "()". --- core/engine/src/object/builtins/jsfunction.rs | 22 +++++ .../src/value/conversions/try_from_js.rs | 6 ++ core/interop/src/lib.rs | 3 +- core/interop/tests/assets/fibonacci.js | 16 ++++ core/interop/tests/assets/gcd_callback.js | 28 ++++++ core/interop/tests/fibonacci.rs | 91 +++++++++++++++++++ core/interop/tests/gcd_callback.rs | 49 ++++++++++ 7 files changed, 214 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 4cc48dba027..f1d581c3839 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -54,6 +54,11 @@ impl TypedJsFunction { 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 { @@ -69,6 +74,23 @@ 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() + }) + .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 { diff --git a/core/engine/src/value/conversions/try_from_js.rs b/core/engine/src/value/conversions/try_from_js.rs index b41645d0f36..ebe7c980fa1 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 2813e9d0469..af282f77d33 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 00000000000..f102043818d --- /dev/null +++ b/core/interop/tests/assets/fibonacci.js @@ -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); +} diff --git a/core/interop/tests/assets/gcd_callback.js b/core/interop/tests/assets/gcd_callback.js new file mode 100644 index 00000000000..c91d79bf6d4 --- /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 00000000000..4764b10fca9 --- /dev/null +++ b/core/interop/tests/fibonacci.rs @@ -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 { + 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 { + 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\"" + ); +} diff --git a/core/interop/tests/gcd_callback.rs b/core/interop/tests/gcd_callback.rs new file mode 100644 index 00000000000..203422519fb --- /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); +} From ba1e384768599dc2775801e949a16f25a2c12ccf Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 5 Sep 2024 12:12:29 -0700 Subject: [PATCH 2/6] Fix clippies and fmt --- core/engine/src/object/builtins/jsfunction.rs | 3 ++- core/interop/tests/fibonacci.rs | 25 ++++++++++--------- core/interop/tests/gcd_callback.rs | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/core/engine/src/object/builtins/jsfunction.rs b/core/engine/src/object/builtins/jsfunction.rs index f1d581c3839..1b05ea841fd 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -55,6 +55,7 @@ impl TypedJsFunction { } /// Get the inner `JsFunction` without consuming this object. + #[must_use] pub fn to_js_function(&self) -> JsFunction { self.inner.clone() } @@ -83,7 +84,7 @@ impl TryFromJs for TypedJsFunction { .with_message("object is not a function") .into() }) - .and_then(|f| Ok(f.typed())), + .map(JsFunction::typed), _ => Err(JsNativeError::typ() .with_message("value is not a Function object") .into()), diff --git a/core/interop/tests/fibonacci.rs b/core/interop/tests/fibonacci.rs index 4764b10fca9..4512d14c425 100644 --- a/core/interop/tests/fibonacci.rs +++ b/core/interop/tests/fibonacci.rs @@ -8,7 +8,8 @@ use boa_engine::{js_error, js_str, Context, JsResult, Module, Source}; use boa_interop::IntoJsFunctionCopied; use std::path::PathBuf; -fn fibonacci_rs( +#[allow(clippy::needless_pass_by_value)] +fn fibonacci( a: usize, cb_a: TypedJsFunction<(usize, JsFunction, JsFunction), usize>, cb_b: TypedJsFunction<(usize, JsFunction, JsFunction), usize>, @@ -17,13 +18,13 @@ fn fibonacci_rs( 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))?) + Ok(cb_a.call( + context, + (a - 1, cb_b.to_js_function(), cb_a.to_js_function()), + )? + cb_b.call( + context, + (a - 2, cb_b.to_js_function(), cb_a.to_js_function()), + )?) } } @@ -36,12 +37,12 @@ fn fibonacci_throw( if a < 5 { Err(js_error!("a is too small")) } else { - fibonacci_rs(a, cb_a, cb_b, context) + fibonacci(a, cb_a, cb_b, context) } } #[test] -fn fibonacci() { +fn fibonacci_test() { let assets_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("tests/assets"); @@ -61,7 +62,7 @@ fn fibonacci() { .get_typed_fn::<(usize, JsFunction, JsFunction), usize>(js_str!("fibonacci"), context) .unwrap(); - let fibonacci_rs = fibonacci_rs + let fibonacci_rust = fibonacci .into_js_function_copied(context) .to_js_function(context.realm()); @@ -69,7 +70,7 @@ fn fibonacci() { fibonacci_js .call( context, - (10, fibonacci_rs.clone(), fibonacci_js.to_js_function()) + (10, fibonacci_rust.clone(), fibonacci_js.to_js_function()) ) .unwrap(), 55 diff --git a/core/interop/tests/gcd_callback.rs b/core/interop/tests/gcd_callback.rs index 203422519fb..359a79cbaf1 100644 --- a/core/interop/tests/gcd_callback.rs +++ b/core/interop/tests/gcd_callback.rs @@ -1,5 +1,5 @@ #![allow(unused_crate_dependencies)] -//! A test that mimics the boa_engine's GCD test with a typed callback. +//! 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}; From 08501bd0b738668145a94b2baa0412158acb1514 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 5 Sep 2024 12:13:46 -0700 Subject: [PATCH 3/6] Prettier --- core/interop/tests/assets/fibonacci.js | 13 ++++++---- core/interop/tests/assets/gcd_callback.js | 30 +++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/interop/tests/assets/fibonacci.js b/core/interop/tests/assets/fibonacci.js index f102043818d..2e22645b767 100644 --- a/core/interop/tests/assets/fibonacci.js +++ b/core/interop/tests/assets/fibonacci.js @@ -7,10 +7,13 @@ * @returns {number} The {a}th fibonacci number. */ export function fibonacci(a, callback_a, callback_b) { - if (a <= 1) { - return a; - } + 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); + // 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 index c91d79bf6d4..b3a744ec3f3 100644 --- a/core/interop/tests/assets/gcd_callback.js +++ b/core/interop/tests/assets/gcd_callback.js @@ -7,22 +7,22 @@ * @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"); - } + 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; + // 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); + let result = inner_gcd(a, b); + callback(result); } From 5d4bc31dfc3eb458612561b2d64445773bb0e178 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 5 Sep 2024 15:30:28 -0700 Subject: [PATCH 4/6] Add From for JsValue to allow conversion --- core/engine/src/object/builtins/jsfunction.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/engine/src/object/builtins/jsfunction.rs b/core/engine/src/object/builtins/jsfunction.rs index 1b05ea841fd..4ec56cebde4 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -92,6 +92,13 @@ impl TryFromJs for TypedJsFunction { } } +impl From> for JsValue { + #[inline] + fn from(o: TypedJsFunction) -> Self { + o.into_inner().into() + } +} + /// JavaScript `Function` rust object. #[derive(Debug, Clone, Trace, Finalize)] pub struct JsFunction { From ff98cde94415d0e7cfa836edc4ced9d821789e0a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 11 Sep 2024 09:40:30 -0700 Subject: [PATCH 5/6] Implement comments --- core/engine/src/object/builtins/jsfunction.rs | 10 ++++++++-- core/interop/tests/fibonacci.rs | 11 ++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/core/engine/src/object/builtins/jsfunction.rs b/core/engine/src/object/builtins/jsfunction.rs index 4ec56cebde4..16ff8fdd898 100644 --- a/core/engine/src/object/builtins/jsfunction.rs +++ b/core/engine/src/object/builtins/jsfunction.rs @@ -56,8 +56,8 @@ impl TypedJsFunction { /// Get the inner `JsFunction` without consuming this object. #[must_use] - pub fn to_js_function(&self) -> JsFunction { - self.inner.clone() + pub fn as_js_function(&self) -> &JsFunction { + &self.inner } /// Call the function with the given arguments. @@ -99,6 +99,12 @@ impl From> for JsValu } } +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/interop/tests/fibonacci.rs b/core/interop/tests/fibonacci.rs index 4512d14c425..db873018b2c 100644 --- a/core/interop/tests/fibonacci.rs +++ b/core/interop/tests/fibonacci.rs @@ -18,13 +18,10 @@ fn fibonacci( if a <= 1 { Ok(a) } else { - Ok(cb_a.call( - context, - (a - 1, cb_b.to_js_function(), cb_a.to_js_function()), - )? + cb_b.call( - context, - (a - 2, cb_b.to_js_function(), cb_a.to_js_function()), - )?) + 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()))?, + ) } } From 831223d4ce695eb2ba22f442214b8ffad5a8e5e4 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 11 Sep 2024 17:25:34 -0700 Subject: [PATCH 6/6] clippies --- core/interop/tests/fibonacci.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/interop/tests/fibonacci.rs b/core/interop/tests/fibonacci.rs index db873018b2c..0ce64e3cab9 100644 --- a/core/interop/tests/fibonacci.rs +++ b/core/interop/tests/fibonacci.rs @@ -67,7 +67,11 @@ fn fibonacci_test() { fibonacci_js .call( context, - (10, fibonacci_rust.clone(), fibonacci_js.to_js_function()) + ( + 10, + fibonacci_rust.clone(), + fibonacci_js.as_js_function().clone() + ) ) .unwrap(), 55 @@ -80,7 +84,11 @@ fn fibonacci_test() { fibonacci_js .call( context, - (10, fibonacci_throw.clone(), fibonacci_js.to_js_function()) + ( + 10, + fibonacci_throw.clone(), + fibonacci_js.as_js_function().clone() + ) ) .unwrap_err() .to_string(),