Browse Source

Add a ContextData struct to inject host defined types from the context (#3802)

* Add more utility traits and funtions to boa_interop

* cargo clippy

* Readd the safe version for Fn which also implements Copy

* Use a new trait for converting a Rust type to a JsResult<JsValue>

* cargo clippy

* Add a test for returning result and Fn()

* Seal both IntoJsFunction and IntoJsFunctionUnsafe

* Try Borrowing and return a nice error instead of panicking

* Address comments

* Rename into_js_function to into_js_function_copied

* Move TryIntoJsResult to boa_engine

* Move JsRest to be at the end only of arguments and add JsAll

* use from_copy_closure and remove unsafe

* Add a HostDefined struct to grab host defined in the argument list

* cargo fmt and clippy

* Explain why we need Clone

* Remove the vector from JsRest<> and use a reference

* Add HostDefined to context as Data, and rename the injector in boa_interop

* Use TypeError instead if the type was not found in context

* Update core/interop/src/lib.rs

Co-authored-by: José Julián Espina <jedel0124@gmail.com>

* cargo fmt

* cargo fmt

---------

Co-authored-by: José Julián Espina <jedel0124@gmail.com>
pull/3821/head
Hans Larsen 7 months ago committed by GitHub
parent
commit
f955248732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 31
      core/engine/src/context/mod.rs
  2. 48
      core/interop/src/into_js_function_impls.rs
  3. 119
      core/interop/src/lib.rs

31
core/engine/src/context/mod.rs

@ -25,7 +25,7 @@ use crate::{
realm::Realm,
script::Script,
vm::{ActiveRunnable, CallFrame, Vm},
JsNativeError, JsResult, JsString, JsValue, Source,
HostDefined, JsNativeError, JsResult, JsString, JsValue, NativeObject, Source,
};
use self::intrinsics::StandardConstructor;
@ -115,6 +115,8 @@ pub struct Context {
/// Unique identifier for each parser instance used during the context lifetime.
parser_identifier: u32,
data: HostDefined,
}
impl std::fmt::Debug for Context {
@ -585,6 +587,32 @@ impl Context {
pub fn can_block(&self) -> bool {
self.can_block
}
/// Insert a type into the context-specific [`HostDefined`] field.
#[inline]
pub fn insert_data<T: NativeObject>(&mut self, value: T) -> Option<Box<T>> {
self.data.insert(value)
}
/// Check if the context-specific [`HostDefined`] has type T.
#[inline]
#[must_use]
pub fn has_data<T: NativeObject>(&self) -> bool {
self.data.has::<T>()
}
/// Remove type T from the context-specific [`HostDefined`], if it exists.
#[inline]
pub fn remove_data<T: NativeObject>(&mut self) -> Option<Box<T>> {
self.data.remove::<T>()
}
/// Get type T from the context-specific [`HostDefined`], if it exists.
#[inline]
#[must_use]
pub fn get_data<T: NativeObject>(&self) -> Option<&T> {
self.data.get::<T>()
}
}
// ==== Private API ====
@ -1070,6 +1098,7 @@ impl ContextBuilder {
root_shape,
parser_identifier: 0,
can_block: self.can_block,
data: HostDefined::default(),
};
builtins::set_default_global_bindings(&mut context)?;

48
core/interop/src/into_js_function_impls.rs

@ -16,35 +16,35 @@ macro_rules! impl_into_js_function {
($($id: ident: $t: ident),*) => {
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)*), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)*) -> R + 'static
{}
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* ContextArgToken,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* &mut Context) -> R + 'static
{}
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest,), R> for T
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest<'_>,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* JsRest) -> R + 'static
T: FnMut($($t,)* JsRest<'_>) -> R + 'static
{}
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest, ContextArgToken), R> for T
impl<$($t,)* R, T> IntoJsFunctionSealed<($($t,)* JsRest<'_>, ContextArgToken), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* JsRest, &mut Context) -> R + 'static
T: FnMut($($t,)* JsRest<'_>, &mut Context) -> R + 'static
{}
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)*), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)*) -> R + 'static,
{
@ -68,11 +68,11 @@ macro_rules! impl_into_js_function {
}
}
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest,), R> for T
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest<'_>,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* JsRest) -> R + 'static,
T: FnMut($($t,)* JsRest<'_>) -> R + 'static,
{
#[allow(unused_variables)]
unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction {
@ -96,7 +96,7 @@ macro_rules! impl_into_js_function {
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* ContextArgToken,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* &mut Context) -> R + 'static,
{
@ -116,11 +116,11 @@ macro_rules! impl_into_js_function {
}
}
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest, ContextArgToken), R> for T
impl<$($t,)* R, T> UnsafeIntoJsFunction<($($t,)* JsRest<'_>, ContextArgToken), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: FnMut($($t,)* JsRest, &mut Context) -> R + 'static,
T: FnMut($($t,)* JsRest<'_>, &mut Context) -> R + 'static,
{
#[allow(unused_variables)]
unsafe fn into_js_function_unsafe(self, _context: &mut Context) -> NativeFunction {
@ -141,7 +141,7 @@ macro_rules! impl_into_js_function {
// Safe versions for `Fn(..) -> ...`.
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)*), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: Fn($($t,)*) -> R + 'static + Copy,
{
@ -159,11 +159,11 @@ macro_rules! impl_into_js_function {
}
}
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest,), R> for T
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest<'_>,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: Fn($($t,)* JsRest) -> R + 'static + Copy,
T: Fn($($t,)* JsRest<'_>) -> R + 'static + Copy,
{
#[allow(unused_variables)]
fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction {
@ -181,7 +181,7 @@ macro_rules! impl_into_js_function {
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* ContextArgToken,), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: Fn($($t,)* &mut Context) -> R + 'static + Copy,
{
@ -199,11 +199,11 @@ macro_rules! impl_into_js_function {
}
}
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest, ContextArgToken), R> for T
impl<$($t,)* R, T> IntoJsFunctionCopied<($($t,)* JsRest<'_>, ContextArgToken), R> for T
where
$($t: TryFromJsArgument + 'static,)*
$($t: for<'a> TryFromJsArgument<'a> + 'static,)*
R: TryIntoJsResult,
T: Fn($($t,)* JsRest, &mut Context) -> R + 'static + Copy,
T: Fn($($t,)* JsRest<'_>, &mut Context) -> R + 'static + Copy,
{
#[allow(unused_variables)]
fn into_js_function_copied(self, _context: &mut Context) -> NativeFunction {

119
core/interop/src/lib.rs

@ -2,7 +2,9 @@
use boa_engine::module::SyntheticModuleInitializer;
use boa_engine::value::TryFromJs;
use boa_engine::{Context, JsResult, JsString, JsValue, Module, NativeFunction};
use boa_engine::{
Context, JsNativeError, JsResult, JsString, JsValue, Module, NativeFunction, NativeObject,
};
pub use boa_macros;
pub mod loaders;
@ -116,21 +118,21 @@ pub trait IntoJsFunctionCopied<Args, Ret>: private::IntoJsFunctionSealed<Args, R
/// Create a Rust value from a JS argument. This trait is used to
/// convert arguments from JS to Rust types. It allows support
/// for optional arguments or rest arguments.
pub trait TryFromJsArgument: Sized {
pub trait TryFromJsArgument<'a>: Sized {
/// Try to convert a JS argument into a Rust value, returning the
/// value and the rest of the arguments to be parsed.
///
/// # Errors
/// Any parsing errors that may occur during the conversion.
fn try_from_js_argument<'a>(
fn try_from_js_argument(
this: &'a JsValue,
rest: &'a [JsValue],
context: &mut Context,
) -> JsResult<(Self, &'a [JsValue])>;
}
impl<T: TryFromJs> TryFromJsArgument for T {
fn try_from_js_argument<'a>(
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for T {
fn try_from_js_argument(
_: &'a JsValue,
rest: &'a [JsValue],
context: &mut Context,
@ -164,26 +166,27 @@ impl<T: TryFromJs> TryFromJsArgument for T {
/// assert_eq!(result, JsValue::new(6));
/// ```
#[derive(Debug, Clone)]
pub struct JsRest(pub Vec<JsValue>);
pub struct JsRest<'a>(pub &'a [JsValue]);
#[allow(unused)]
impl JsRest {
impl<'a> JsRest<'a> {
/// Consumes the `JsRest` and returns the inner list of `JsValue`.
#[must_use]
pub fn into_inner(self) -> Vec<JsValue> {
pub fn into_inner(self) -> &'a [JsValue] {
self.0
}
/// Transforms the `JsRest` into a `Vec<JsValue>`.
#[must_use]
pub fn to_vec(self) -> Vec<JsValue> {
self.0.to_vec()
}
/// Returns an iterator over the inner list of `JsValue`.
pub fn iter(&self) -> impl Iterator<Item = &JsValue> {
self.0.iter()
}
/// Returns a mutable iterator over the inner list of `JsValue`.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JsValue> {
self.0.iter_mut()
}
/// Returns the length of the inner list of `JsValue`.
#[must_use]
pub fn len(&self) -> usize {
@ -197,18 +200,18 @@ impl JsRest {
}
}
impl From<&[JsValue]> for JsRest {
fn from(values: &[JsValue]) -> Self {
Self(values.to_vec())
impl<'a> From<&'a [JsValue]> for JsRest<'a> {
fn from(values: &'a [JsValue]) -> Self {
Self(values)
}
}
impl IntoIterator for JsRest {
type Item = JsValue;
type IntoIter = std::vec::IntoIter<JsValue>;
impl<'a> IntoIterator for JsRest<'a> {
type Item = &'a JsValue;
type IntoIter = std::slice::Iter<'a, JsValue>;
fn into_iter(self) -> Self::IntoIter {
self.into_inner().into_iter()
self.into_inner().iter()
}
}
@ -265,8 +268,8 @@ impl<T: TryFromJs> JsAll<T> {
}
}
impl<T: TryFromJs> TryFromJsArgument for JsAll<T> {
fn try_from_js_argument<'a>(
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsAll<T> {
fn try_from_js_argument(
_this: &'a JsValue,
mut rest: &'a [JsValue],
context: &mut Context,
@ -292,8 +295,8 @@ impl<T: TryFromJs> TryFromJsArgument for JsAll<T> {
#[derive(Debug, Clone)]
pub struct JsThis<T: TryFromJs>(pub T);
impl<T: TryFromJs> TryFromJsArgument for JsThis<T> {
fn try_from_js_argument<'a>(
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsThis<T> {
fn try_from_js_argument(
this: &'a JsValue,
rest: &'a [JsValue],
context: &mut Context,
@ -302,6 +305,50 @@ impl<T: TryFromJs> TryFromJsArgument for JsThis<T> {
}
}
/// Captures a [`ContextData`] data from the [`Context`] as a JS function argument,
/// based on its type.
///
/// The host defined type must implement [`Clone`], otherwise the borrow
/// checker would not be able to ensure the safety of the context while
/// making the function call. Because of this, it is recommended to use
/// types that are cheap to clone.
///
/// For example,
/// ```
/// # use boa_engine::{Context, Finalize, JsData, JsValue, Trace};
/// use boa_interop::{IntoJsFunctionCopied, ContextData};
///
/// #[derive(Clone, Debug, Finalize, JsData, Trace)]
/// struct CustomHostDefinedStruct {
/// #[unsafe_ignore_trace]
/// pub counter: usize,
/// }
/// let mut context = Context::default();
/// context.insert_data(CustomHostDefinedStruct { counter: 123 });
/// let f = (|ContextData(host): ContextData<CustomHostDefinedStruct>| {
/// host.counter + 1
/// }).into_js_function_copied(&mut context);
///
/// assert_eq!(f.call(&JsValue::undefined(), &[], &mut context), Ok(JsValue::new(124)));
/// ```
#[derive(Debug, Clone)]
pub struct ContextData<T: Clone>(pub T);
impl<'a, T: NativeObject + Clone> TryFromJsArgument<'a> for ContextData<T> {
fn try_from_js_argument(
_this: &'a JsValue,
rest: &'a [JsValue],
context: &mut Context,
) -> JsResult<(Self, &'a [JsValue])> {
match context.get_data::<T>() {
Some(value) => Ok((ContextData(value.clone()), rest)),
None => Err(JsNativeError::typ()
.with_message("Context data not found")
.into()),
}
}
}
// Implement `IntoJsFunction` for functions with a various list of
// arguments.
mod into_js_function_impls;
@ -310,9 +357,12 @@ mod into_js_function_impls;
#[allow(clippy::missing_panics_doc)]
pub fn into_js_module() {
use boa_engine::{js_string, JsValue, Source};
use boa_gc::{Gc, GcRefCell};
use std::cell::RefCell;
use std::rc::Rc;
type ResultType = Gc<GcRefCell<JsValue>>;
let loader = Rc::new(loaders::HashMapModuleLoader::new());
let mut context = Context::builder()
.module_loader(loader.clone())
@ -322,7 +372,9 @@ pub fn into_js_module() {
let foo_count = Rc::new(RefCell::new(0));
let bar_count = Rc::new(RefCell::new(0));
let dad_count = Rc::new(RefCell::new(0));
let result = Rc::new(RefCell::new(JsValue::undefined()));
context.insert_data(Gc::new(GcRefCell::new(JsValue::undefined())));
let module = unsafe {
vec![
(
@ -354,7 +406,7 @@ pub fn into_js_module() {
UnsafeIntoJsFunction::into_js_function_unsafe(
{
let counter = dad_count.clone();
move |args: JsRest, context: &mut Context| {
move |args: JsRest<'_>, context: &mut Context| {
*counter.borrow_mut() += args
.into_iter()
.map(|i| i.try_js_into::<i32>(context).unwrap())
@ -366,15 +418,10 @@ pub fn into_js_module() {
),
(
js_string!("send"),
UnsafeIntoJsFunction::into_js_function_unsafe(
{
let result = result.clone();
move |value: JsValue| {
(move |value: JsValue, ContextData(result): ContextData<ResultType>| {
*result.borrow_mut() = value;
}
},
&mut context,
),
})
.into_js_function_copied(&mut context),
),
]
}
@ -409,10 +456,12 @@ pub fn into_js_module() {
promise_result.state()
);
let result = context.get_data::<ResultType>().unwrap().borrow().clone();
assert_eq!(*foo_count.borrow(), 2);
assert_eq!(*bar_count.borrow(), 15);
assert_eq!(*dad_count.borrow(), 24);
assert_eq!(result.borrow().clone().try_js_into(&mut context), Ok(1u32));
assert_eq!(result.try_js_into(&mut context), Ok(1u32));
}
#[test]

Loading…
Cancel
Save