Browse Source

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<TypedJsFunction> for JsValue to allow conversion

* Implement comments

* clippies
pull/4003/head
Hans Larsen 3 months ago committed by GitHub
parent
commit
8438ad21cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 36
      core/engine/src/object/builtins/jsfunction.rs
  2. 6
      core/engine/src/value/conversions/try_from_js.rs
  3. 3
      core/interop/src/lib.rs
  4. 19
      core/interop/tests/assets/fibonacci.js
  5. 28
      core/interop/tests/assets/gcd_callback.js
  6. 97
      core/interop/tests/fibonacci.rs
  7. 49
      core/interop/tests/gcd_callback.rs

36
core/engine/src/object/builtins/jsfunction.rs

@ -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> {
@ -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 {

6
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<Self> {
Ok(())
}
}
impl TryFromJs for String {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {

3
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;

19
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)
);
}

28
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);
}

97
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<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
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<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…
Cancel
Save