Skip to content

Commit c1db55b

Browse files
authored
Move interop and module utilities from boa_interop into boa_engine (#4218)
Migrated module and function interop traits and JS argument handling functionality into the `boa_engine` crate for better modularity. Deprecated certain exports in `boa_interop` and updated references across the project to reflect the changes, to maintain backward compatibility, at least until we kill `boa_interop` for good.
1 parent 4341452 commit c1db55b

File tree

7 files changed

+488
-478
lines changed

7 files changed

+488
-478
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
use boa_engine::object::Object;
2+
use boa_engine::value::TryFromJs;
3+
use boa_engine::{Context, JsNativeError, JsResult, JsValue, NativeObject};
4+
use boa_gc::{GcRef, GcRefMut};
5+
use std::ops::Deref;
6+
7+
/// Create a Rust value from a JS argument. This trait is used to
8+
/// convert arguments from JS to Rust types. It allows support
9+
/// for optional arguments or rest arguments.
10+
pub trait TryFromJsArgument<'a>: Sized {
11+
/// Try to convert a JS argument into a Rust value, returning the
12+
/// value and the rest of the arguments to be parsed.
13+
///
14+
/// # Errors
15+
/// Any parsing errors that may occur during the conversion.
16+
fn try_from_js_argument(
17+
this: &'a JsValue,
18+
rest: &'a [JsValue],
19+
context: &mut Context,
20+
) -> JsResult<(Self, &'a [JsValue])>;
21+
}
22+
23+
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for T {
24+
fn try_from_js_argument(
25+
_: &'a JsValue,
26+
rest: &'a [JsValue],
27+
context: &mut Context,
28+
) -> JsResult<(Self, &'a [JsValue])> {
29+
match rest.split_first() {
30+
Some((first, rest)) => Ok((first.try_js_into(context)?, rest)),
31+
None => T::try_from_js(&JsValue::undefined(), context).map(|v| (v, rest)),
32+
}
33+
}
34+
}
35+
36+
/// An argument that would be ignored in a JS function. This is equivalent of typing
37+
/// `()` in Rust functions argument, but more explicit.
38+
#[derive(Debug, Clone, Copy)]
39+
pub struct Ignore;
40+
41+
impl<'a> TryFromJsArgument<'a> for Ignore {
42+
fn try_from_js_argument(
43+
_this: &'a JsValue,
44+
rest: &'a [JsValue],
45+
_: &mut Context,
46+
) -> JsResult<(Self, &'a [JsValue])> {
47+
Ok((Ignore, &rest[1..]))
48+
}
49+
}
50+
51+
/// An argument that when used in a JS function will empty the list
52+
/// of JS arguments as `JsValue`s. This can be used for having the
53+
/// rest of the arguments in a function. It should be the last
54+
/// argument of your function, before the `Context` argument if any.
55+
///
56+
/// For example,
57+
/// ```
58+
/// # use boa_engine::{Context, JsValue, IntoJsFunctionCopied};
59+
/// # use boa_engine::interop::JsRest;
60+
/// # let mut context = Context::default();
61+
/// let sums = (|args: JsRest, context: &mut Context| -> i32 {
62+
/// args.iter()
63+
/// .map(|i| i.try_js_into::<i32>(context).unwrap())
64+
/// .sum::<i32>()
65+
/// })
66+
/// .into_js_function_copied(&mut context);
67+
///
68+
/// let result = sums
69+
/// .call(
70+
/// &JsValue::undefined(),
71+
/// &[JsValue::from(1), JsValue::from(2), JsValue::from(3)],
72+
/// &mut context,
73+
/// )
74+
/// .unwrap();
75+
/// assert_eq!(result, JsValue::new(6));
76+
/// ```
77+
#[derive(Debug, Clone)]
78+
pub struct JsRest<'a>(pub &'a [JsValue]);
79+
80+
#[allow(unused)]
81+
impl<'a> JsRest<'a> {
82+
/// Consumes the `JsRest` and returns the inner list of `JsValue`.
83+
#[must_use]
84+
pub fn into_inner(self) -> &'a [JsValue] {
85+
self.0
86+
}
87+
88+
/// Transforms the `JsRest` into a `Vec<JsValue>`.
89+
#[must_use]
90+
pub fn to_vec(self) -> Vec<JsValue> {
91+
self.0.to_vec()
92+
}
93+
94+
/// Returns an iterator over the inner list of `JsValue`.
95+
pub fn iter(&self) -> impl Iterator<Item = &JsValue> {
96+
self.0.iter()
97+
}
98+
99+
/// Returns the length of the inner list of `JsValue`.
100+
#[must_use]
101+
pub fn len(&self) -> usize {
102+
self.0.len()
103+
}
104+
105+
/// Returns `true` if the inner list of `JsValue` is empty.
106+
#[must_use]
107+
pub fn is_empty(&self) -> bool {
108+
self.0.is_empty()
109+
}
110+
}
111+
112+
impl<'a> From<&'a [JsValue]> for JsRest<'a> {
113+
fn from(values: &'a [JsValue]) -> Self {
114+
Self(values)
115+
}
116+
}
117+
118+
impl<'a> IntoIterator for JsRest<'a> {
119+
type Item = &'a JsValue;
120+
type IntoIter = std::slice::Iter<'a, JsValue>;
121+
122+
fn into_iter(self) -> Self::IntoIter {
123+
self.into_inner().iter()
124+
}
125+
}
126+
127+
/// An argument that when used in a JS function will capture all
128+
/// the arguments that can be converted to `T`. The first argument
129+
/// that cannot be converted to `T` will stop the conversion.
130+
///
131+
/// For example,
132+
/// ```
133+
/// # use boa_engine::{Context, JsValue, IntoJsFunctionCopied};
134+
/// # use boa_engine::interop::JsAll;
135+
/// # let mut context = Context::default();
136+
/// let sums = (|args: JsAll<i32>, context: &mut Context| -> i32 { args.iter().sum() })
137+
/// .into_js_function_copied(&mut context);
138+
///
139+
/// let result = sums
140+
/// .call(
141+
/// &JsValue::undefined(),
142+
/// &[
143+
/// JsValue::from(1),
144+
/// JsValue::from(2),
145+
/// JsValue::from(3),
146+
/// JsValue::from(true),
147+
/// JsValue::from(4),
148+
/// ],
149+
/// &mut context,
150+
/// )
151+
/// .unwrap();
152+
/// assert_eq!(result, JsValue::new(6));
153+
/// ```
154+
#[derive(Debug, Clone)]
155+
pub struct JsAll<T: TryFromJs>(pub Vec<T>);
156+
157+
impl<T: TryFromJs> JsAll<T> {
158+
/// Consumes the `JsAll` and returns the inner list of `T`.
159+
#[must_use]
160+
pub fn into_inner(self) -> Vec<T> {
161+
self.0
162+
}
163+
164+
/// Returns an iterator over the inner list of `T`.
165+
pub fn iter(&self) -> impl Iterator<Item = &T> {
166+
self.0.iter()
167+
}
168+
169+
/// Returns a mutable iterator over the inner list of `T`.
170+
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
171+
self.0.iter_mut()
172+
}
173+
174+
/// Returns the length of the inner list of `T`.
175+
#[must_use]
176+
pub fn len(&self) -> usize {
177+
self.0.len()
178+
}
179+
180+
/// Returns `true` if the inner list of `T` is empty.
181+
#[must_use]
182+
pub fn is_empty(&self) -> bool {
183+
self.0.is_empty()
184+
}
185+
}
186+
187+
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsAll<T> {
188+
fn try_from_js_argument(
189+
_this: &'a JsValue,
190+
mut rest: &'a [JsValue],
191+
context: &mut Context,
192+
) -> JsResult<(Self, &'a [JsValue])> {
193+
let mut values = Vec::new();
194+
195+
while !rest.is_empty() {
196+
match rest[0].try_js_into(context) {
197+
Ok(value) => {
198+
values.push(value);
199+
rest = &rest[1..];
200+
}
201+
Err(_) => break,
202+
}
203+
}
204+
Ok((JsAll(values), rest))
205+
}
206+
}
207+
208+
/// Captures the `this` value in a JS function. Although this can be
209+
/// specified multiple times as argument, it will always be filled
210+
/// with clone of the same value.
211+
#[derive(Debug, Clone)]
212+
pub struct JsThis<T: TryFromJs>(pub T);
213+
214+
impl<'a, T: TryFromJs> TryFromJsArgument<'a> for JsThis<T> {
215+
fn try_from_js_argument(
216+
this: &'a JsValue,
217+
rest: &'a [JsValue],
218+
context: &mut Context,
219+
) -> JsResult<(Self, &'a [JsValue])> {
220+
Ok((JsThis(this.try_js_into(context)?), rest))
221+
}
222+
}
223+
224+
impl<T: TryFromJs> Deref for JsThis<T> {
225+
type Target = T;
226+
227+
fn deref(&self) -> &Self::Target {
228+
&self.0
229+
}
230+
}
231+
232+
/// Captures a class instance from the `this` value in a JS function. The class
233+
/// will be a non-mutable reference of Rust type `T`, if it is an instance of `T`.
234+
///
235+
/// To have more flexibility on the parsing of the `this` value, you can use the
236+
/// [`JsThis`] capture instead.
237+
#[derive(Debug, Clone)]
238+
pub struct JsClass<T: NativeObject> {
239+
inner: boa_engine::JsObject<T>,
240+
}
241+
242+
impl<T: NativeObject> JsClass<T> {
243+
/// Borrow a reference to the class instance of type `T`.
244+
///
245+
/// # Panics
246+
///
247+
/// Panics if the object is currently borrowed.
248+
///
249+
/// This does not panic if the type is wrong, as the type is checked
250+
/// during the construction of the `JsClass` instance.
251+
#[must_use]
252+
pub fn borrow(&self) -> GcRef<'_, T> {
253+
GcRef::map(self.inner.borrow(), |obj| obj.data())
254+
}
255+
256+
/// Borrow a mutable reference to the class instance of type `T`.
257+
///
258+
/// # Panics
259+
///
260+
/// Panics if the object is currently mutably borrowed.
261+
#[must_use]
262+
pub fn borrow_mut(&self) -> GcRefMut<'_, Object<T>, T> {
263+
GcRefMut::map(self.inner.borrow_mut(), |obj| obj.data_mut())
264+
}
265+
}
266+
267+
impl<T: NativeObject + Clone> JsClass<T> {
268+
/// Clones the inner class instance.
269+
///
270+
/// # Panics
271+
///
272+
/// Panics if the inner object is currently borrowed mutably.
273+
#[must_use]
274+
pub fn clone_inner(&self) -> T {
275+
self.inner.borrow().data().clone()
276+
}
277+
}
278+
279+
impl<'a, T: NativeObject + 'static> TryFromJsArgument<'a> for JsClass<T> {
280+
fn try_from_js_argument(
281+
this: &'a JsValue,
282+
rest: &'a [JsValue],
283+
_context: &mut Context,
284+
) -> JsResult<(Self, &'a [JsValue])> {
285+
if let Some(object) = this.as_object() {
286+
if let Ok(inner) = object.clone().downcast::<T>() {
287+
return Ok((JsClass { inner }, rest));
288+
}
289+
}
290+
291+
Err(JsNativeError::typ()
292+
.with_message("invalid this for class method")
293+
.into())
294+
}
295+
}
296+
297+
/// Captures a [`ContextData`] data from the [`Context`] as a JS function argument,
298+
/// based on its type.
299+
///
300+
/// The host defined type must implement [`Clone`], otherwise the borrow
301+
/// checker would not be able to ensure the safety of the context while
302+
/// making the function call. Because of this, it is recommended to use
303+
/// types that are cheap to clone.
304+
///
305+
/// For example,
306+
/// ```
307+
/// # use boa_engine::{Context, Finalize, JsData, JsValue, Trace, IntoJsFunctionCopied};
308+
/// # use boa_engine::interop::ContextData;
309+
///
310+
/// #[derive(Clone, Debug, Finalize, JsData, Trace)]
311+
/// struct CustomHostDefinedStruct {
312+
/// #[unsafe_ignore_trace]
313+
/// pub counter: usize,
314+
/// }
315+
/// let mut context = Context::default();
316+
/// context.insert_data(CustomHostDefinedStruct { counter: 123 });
317+
/// let f = (|ContextData(host): ContextData<CustomHostDefinedStruct>| host.counter + 1)
318+
/// .into_js_function_copied(&mut context);
319+
///
320+
/// assert_eq!(
321+
/// f.call(&JsValue::undefined(), &[], &mut context),
322+
/// Ok(JsValue::new(124))
323+
/// );
324+
/// ```
325+
#[derive(Debug, Clone)]
326+
pub struct ContextData<T: Clone>(pub T);
327+
328+
impl<'a, T: NativeObject + Clone> TryFromJsArgument<'a> for ContextData<T> {
329+
fn try_from_js_argument(
330+
_this: &'a JsValue,
331+
rest: &'a [JsValue],
332+
context: &mut Context,
333+
) -> JsResult<(Self, &'a [JsValue])> {
334+
match context.get_data::<T>() {
335+
Some(value) => Ok((ContextData(value.clone()), rest)),
336+
None => Err(JsNativeError::typ()
337+
.with_message("Context data not found")
338+
.into()),
339+
}
340+
}
341+
}

core/interop/src/into_js_function_impls.rs renamed to core/engine/src/interop/into_js_function_impls.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! Implementations of the `IntoJsFunction` trait for various function signatures.
22
3+
use super::private::IntoJsFunctionSealed;
4+
use super::{IntoJsFunctionCopied, UnsafeIntoJsFunction};
5+
use crate::interop::{JsRest, TryFromJsArgument};
6+
use crate::{js_string, Context, JsError, NativeFunction, TryIntoJsResult};
37
use std::cell::RefCell;
48

5-
use boa_engine::{js_string, Context, JsError, NativeFunction, TryIntoJsResult};
6-
7-
use crate::private::IntoJsFunctionSealed;
8-
use crate::{IntoJsFunctionCopied, JsRest, TryFromJsArgument, UnsafeIntoJsFunction};
9-
109
/// A token to represent the context argument in the function signature.
1110
/// This should not be used directly and has no external meaning.
1211
#[derive(Debug, Copy, Clone)]

0 commit comments

Comments
 (0)