Skip to main content

boa_engine/object/builtins/
jsfunction.rs

1//! A Rust API wrapper for Boa's `Function` Builtin ECMAScript Object
2use crate::js_string;
3use crate::{
4    Context, JsNativeError, JsResult, JsValue, NativeFunction, TryIntoJsResult,
5    builtins::function::ConstructorKind, native_function::NativeFunctionObject, object::JsObject,
6    value::TryFromJs,
7};
8use boa_gc::{Finalize, Trace};
9use std::marker::PhantomData;
10use std::ops::Deref;
11
12/// A trait for converting a tuple of Rust values into a vector of `JsValue`,
13/// to be used as arguments for a JavaScript function.
14pub trait TryIntoJsArguments {
15    /// Convert a tuple of Rust values into a vector of `JsValue`.
16    /// This is automatically implemented for tuples that implement
17    /// `TryIntoJsResult`.
18    fn into_js_args(self, cx: &mut Context) -> JsResult<Vec<JsValue>>;
19}
20
21macro_rules! impl_try_into_js_args {
22    ($($n: ident: $t: ident),*) => {
23        impl<$($t),*> TryIntoJsArguments for ($($t,)*) where $($t: TryIntoJsResult),* {
24            fn into_js_args(self, cx: &mut Context) -> JsResult<Vec<JsValue>> {
25                let ($($n,)*) = self;
26                Ok(vec![$($n.try_into_js_result(cx)?),*])
27            }
28        }
29    };
30}
31
32impl_try_into_js_args!(a: A);
33impl_try_into_js_args!(a: A, b: B);
34impl_try_into_js_args!(a: A, b: B, c: C);
35impl_try_into_js_args!(a: A, b: B, c: C, d: D);
36impl_try_into_js_args!(a: A, b: B, c: C, d: D, e: E);
37
38/// A JavaScript `Function` rust object, typed. This adds types to
39/// a JavaScript exported function, allowing for type checking and
40/// type conversion in Rust. Those types must convert to a [`JsValue`]
41/// but will not be verified at runtime (since JavaScript doesn't
42/// actually have strong typing).
43///
44/// To create this type, use the [`JsFunction::typed`] method.
45#[derive(Debug, Clone, Trace, Finalize)]
46pub struct TypedJsFunction<A: TryIntoJsArguments, R: TryFromJs> {
47    inner: JsFunction,
48    _args: PhantomData<A>,
49    _ret: PhantomData<R>,
50}
51
52impl<A: TryIntoJsArguments, R: TryFromJs> TypedJsFunction<A, R> {
53    /// Transforms this typed function back into a regular `JsFunction`.
54    #[must_use]
55    pub fn into_inner(self) -> JsFunction {
56        self.inner.clone()
57    }
58
59    /// Get the inner `JsFunction` without consuming this object.
60    #[must_use]
61    pub fn as_js_function(&self) -> &JsFunction {
62        &self.inner
63    }
64
65    /// Call the function with the given arguments.
66    #[inline]
67    #[cfg_attr(feature = "native-backtrace", track_caller)]
68    pub fn call(&self, context: &mut Context, args: A) -> JsResult<R> {
69        self.call_with_this(&JsValue::undefined(), context, args)
70    }
71
72    /// Call the function with the given argument and `this`.
73    #[inline]
74    #[cfg_attr(feature = "native-backtrace", track_caller)]
75    pub fn call_with_this(&self, this: &JsValue, context: &mut Context, args: A) -> JsResult<R> {
76        let arguments = args.into_js_args(context)?;
77        let result = self.inner.call(this, &arguments, context)?;
78        R::try_from_js(&result, context)
79    }
80}
81
82impl<A: TryIntoJsArguments, R: TryFromJs> TryFromJs for TypedJsFunction<A, R> {
83    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
84        if let Some(o) = value.as_object() {
85            JsFunction::from_object(o.clone())
86                .ok_or_else(|| {
87                    JsNativeError::typ()
88                        .with_message("object is not a function")
89                        .into()
90                })
91                .map(JsFunction::typed)
92        } else {
93            Err(JsNativeError::typ()
94                .with_message("value is not a Function object")
95                .into())
96        }
97    }
98}
99
100impl<A: TryIntoJsArguments, R: TryFromJs> From<TypedJsFunction<A, R>> for JsValue {
101    #[inline]
102    fn from(o: TypedJsFunction<A, R>) -> Self {
103        o.into_inner().into()
104    }
105}
106
107impl<A: TryIntoJsArguments, R: TryFromJs> From<TypedJsFunction<A, R>> for JsFunction {
108    fn from(value: TypedJsFunction<A, R>) -> Self {
109        value.inner.clone()
110    }
111}
112
113/// JavaScript `Function` rust object.
114#[derive(Debug, Clone, Trace, Finalize)]
115pub struct JsFunction {
116    inner: JsObject,
117}
118
119impl JsFunction {
120    /// Creates a new `JsFunction` from an object, without checking if the object is callable.
121    pub(crate) fn from_object_unchecked(object: JsObject) -> Self {
122        Self { inner: object }
123    }
124
125    /// Creates a new, empty intrinsic function object with only its function internal methods set.
126    ///
127    /// Mainly used to initialize objects before a [`Context`] is available to do so.
128    ///
129    /// [`Context`]: crate::Context
130    pub(crate) fn empty_intrinsic_function(constructor: bool) -> Self {
131        Self {
132            inner: JsObject::from_proto_and_data(
133                None,
134                NativeFunctionObject {
135                    f: NativeFunction::from_fn_ptr(|_, _, _| Ok(JsValue::undefined())),
136                    name: js_string!(),
137                    constructor: constructor.then_some(ConstructorKind::Base),
138                    realm: None,
139                },
140            ),
141        }
142    }
143
144    /// Creates a [`JsFunction`] from a [`JsObject`], or returns `None` if the object is not a function.
145    ///
146    /// This does not clone the fields of the function, it only does a shallow clone of the object.
147    #[inline]
148    #[must_use]
149    pub fn from_object(object: JsObject) -> Option<Self> {
150        object
151            .is_callable()
152            .then(|| Self::from_object_unchecked(object))
153    }
154
155    /// Creates a `TypedJsFunction` from a `JsFunction`.
156    #[inline]
157    #[must_use]
158    pub fn typed<A: TryIntoJsArguments, R: TryFromJs>(self) -> TypedJsFunction<A, R> {
159        TypedJsFunction {
160            inner: self,
161            _args: PhantomData,
162            _ret: PhantomData,
163        }
164    }
165}
166
167impl From<JsFunction> for JsObject {
168    #[inline]
169    fn from(o: JsFunction) -> Self {
170        o.inner.clone()
171    }
172}
173
174impl From<JsFunction> for JsValue {
175    #[inline]
176    fn from(o: JsFunction) -> Self {
177        o.inner.clone().into()
178    }
179}
180
181impl Deref for JsFunction {
182    type Target = JsObject;
183
184    #[inline]
185    fn deref(&self) -> &Self::Target {
186        &self.inner
187    }
188}
189
190impl TryFromJs for JsFunction {
191    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
192        if let Some(o) = value.as_object() {
193            Self::from_object(o.clone()).ok_or_else(|| {
194                JsNativeError::typ()
195                    .with_message("object is not a function")
196                    .into()
197            })
198        } else {
199            Err(JsNativeError::typ()
200                .with_message("value is not a Function object")
201                .into())
202        }
203    }
204}