Skip to main content

boa_engine/native_function/
mod.rs

1//! Boa's wrappers for native Rust functions to be compatible with ECMAScript calls.
2//!
3//! [`NativeFunction`] is the main type of this module, providing APIs to create native callables
4//! from native Rust functions and closures.
5
6use std::cell::RefCell;
7
8use boa_gc::{Finalize, Gc, Trace, custom_trace};
9use boa_string::JsString;
10
11use crate::job::NativeAsyncJob;
12use crate::object::internal_methods::InternalMethodCallContext;
13use crate::value::JsVariant;
14use crate::{
15    Context, JsNativeError, JsObject, JsResult, JsValue,
16    builtins::{OrdinaryObject, function::ConstructorKind},
17    context::intrinsics::StandardConstructors,
18    object::{
19        FunctionObjectBuilder, JsData, JsFunction, JsPromise,
20        internal_methods::{
21            CallValue, InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
22            get_prototype_from_constructor,
23        },
24    },
25    realm::Realm,
26};
27
28mod continuation;
29
30pub(crate) use continuation::{CoroutineBranch, CoroutineState, NativeCoroutine};
31
32/// The required signature for all native built-in function pointers.
33///
34/// # Arguments
35///
36/// - The first argument represents the `this` variable of every ECMAScript function.
37///
38/// - The second argument represents the list of all arguments passed to the function.
39///
40/// - The last argument is the engine [`Context`].
41pub type NativeFunctionPointer = fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>;
42
43trait TraceableClosure: Trace {
44    fn call(&self, this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue>;
45}
46
47#[derive(Trace, Finalize)]
48struct Closure<F, T>
49where
50    F: Fn(&JsValue, &[JsValue], &T, &mut Context) -> JsResult<JsValue>,
51    T: Trace,
52{
53    // SAFETY: `NativeFunction`'s safe API ensures only `Copy` closures are stored; its unsafe API,
54    // on the other hand, explains the invariants to hold in order for this to be safe, shifting
55    // the responsibility to the caller.
56    #[unsafe_ignore_trace]
57    f: F,
58    captures: T,
59}
60
61impl<F, T> TraceableClosure for Closure<F, T>
62where
63    F: Fn(&JsValue, &[JsValue], &T, &mut Context) -> JsResult<JsValue>,
64    T: Trace,
65{
66    fn call(&self, this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
67        (self.f)(this, args, &self.captures, context)
68    }
69}
70
71#[derive(Clone, Debug, Finalize)]
72/// The data of an object containing a `NativeFunction`.
73pub struct NativeFunctionObject {
74    /// The rust function.
75    pub(crate) f: NativeFunction,
76
77    /// JavaScript name of the function.
78    pub(crate) name: JsString,
79
80    /// The kind of the function constructor if it is a constructor.
81    pub(crate) constructor: Option<ConstructorKind>,
82
83    /// The [`Realm`] in which the function is defined, or `None` if the realm is uninitialized.
84    pub(crate) realm: Option<Realm>,
85}
86
87// SAFETY: this traces all fields that need to be traced by the GC.
88unsafe impl Trace for NativeFunctionObject {
89    custom_trace!(this, mark, {
90        mark(&this.f);
91        mark(&this.realm);
92    });
93}
94
95impl JsData for NativeFunctionObject {
96    fn internal_methods(&self) -> &'static InternalObjectMethods {
97        static FUNCTION: InternalObjectMethods = InternalObjectMethods {
98            __call__: native_function_call,
99            ..ORDINARY_INTERNAL_METHODS
100        };
101
102        static CONSTRUCTOR: InternalObjectMethods = InternalObjectMethods {
103            __call__: native_function_call,
104            __construct__: native_function_construct,
105            ..ORDINARY_INTERNAL_METHODS
106        };
107
108        if self.constructor.is_some() {
109            &CONSTRUCTOR
110        } else {
111            &FUNCTION
112        }
113    }
114}
115
116/// A callable Rust function that can be invoked by the engine.
117///
118/// `NativeFunction` functions are divided in two:
119/// - Function pointers a.k.a common functions (see [`NativeFunctionPointer`]).
120/// - Closure functions that can capture the current environment.
121///
122/// # Caveats
123///
124/// By limitations of the Rust language, the garbage collector currently cannot inspect closures
125/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
126/// to use. All other closures can also be stored in a `NativeFunction`, albeit by using an `unsafe`
127/// API, but note that passing closures implicitly capturing traceable types could cause
128/// **Undefined Behaviour**.
129#[derive(Clone, Finalize)]
130pub struct NativeFunction {
131    inner: Inner,
132}
133
134#[derive(Clone)]
135enum Inner {
136    PointerFn(NativeFunctionPointer),
137    Closure(Gc<dyn TraceableClosure>),
138}
139
140// Manual implementation because deriving `Trace` triggers the `single_use_lifetimes` lint.
141// SAFETY: Only closures can contain `Trace` captures, so this implementation is safe.
142unsafe impl Trace for NativeFunction {
143    custom_trace!(this, mark, {
144        if let Inner::Closure(c) = &this.inner {
145            mark(c);
146        }
147    });
148}
149
150impl std::fmt::Debug for NativeFunction {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("NativeFunction").finish_non_exhaustive()
153    }
154}
155
156impl NativeFunction {
157    /// Creates a `NativeFunction` from a function pointer.
158    #[inline]
159    pub fn from_fn_ptr(function: NativeFunctionPointer) -> Self {
160        Self {
161            inner: Inner::PointerFn(function),
162        }
163    }
164
165    /// Creates a `NativeFunction` from a function returning a [`Future`]-like.
166    ///
167    /// The returned `NativeFunction` will return an ECMAScript `Promise` that will be fulfilled
168    /// or rejected when the returned `Future` completes.
169    ///
170    /// If you only need to convert a `Future`-like into a [`JsPromise`], see
171    /// [`JsPromise::from_async_fn`].
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// # use std::cell::RefCell;
177    /// # use boa_engine::{
178    /// #   JsValue,
179    /// #   Context,
180    /// #   JsResult,
181    /// #   NativeFunction,
182    /// #   JsArgs,
183    /// # };
184    /// async fn test(
185    ///     _this: &JsValue,
186    ///     args: &[JsValue],
187    ///     context: &RefCell<&mut Context>,
188    /// ) -> JsResult<JsValue> {
189    ///     let arg = args.get_or_undefined(0).clone();
190    ///     std::future::ready(()).await;
191    ///     let value = arg.to_u32(&mut context.borrow_mut())?;
192    ///     Ok(JsValue::from(value * 2))
193    /// }
194    /// NativeFunction::from_async_fn(test);
195    /// ```
196    pub fn from_async_fn<F>(f: F) -> Self
197    where
198        F: AsyncFn(&JsValue, &[JsValue], &RefCell<&mut Context>) -> JsResult<JsValue> + 'static,
199        F: Copy,
200    {
201        Self::from_copy_closure(move |this, args, context| {
202            let (promise, resolvers) = JsPromise::new_pending(context);
203            let this = this.clone();
204            let args = args.to_vec();
205
206            context.enqueue_job(
207                NativeAsyncJob::new(async move |context| {
208                    let result = f(&this, &args, context).await;
209
210                    let context = &mut context.borrow_mut();
211                    match result {
212                        Ok(v) => resolvers.resolve.call(&JsValue::undefined(), &[v], context),
213                        Err(e) => {
214                            let e = e.into_opaque(context)?;
215                            resolvers.reject.call(&JsValue::undefined(), &[e], context)
216                        }
217                    }
218                })
219                .into(),
220            );
221
222            Ok(promise.into())
223        })
224    }
225
226    /// Creates a `NativeFunction` from a `Copy` closure.
227    pub fn from_copy_closure<F>(closure: F) -> Self
228    where
229        F: Fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue> + Copy + 'static,
230    {
231        // SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
232        unsafe { Self::from_closure(closure) }
233    }
234
235    /// Creates a `NativeFunction` from a `Copy` closure and a list of traceable captures.
236    pub fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self
237    where
238        F: Fn(&JsValue, &[JsValue], &T, &mut Context) -> JsResult<JsValue> + Copy + 'static,
239        T: Trace + 'static,
240    {
241        // SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
242        unsafe { Self::from_closure_with_captures(closure, captures) }
243    }
244
245    /// Creates a new `NativeFunction` from a closure.
246    ///
247    /// # Safety
248    ///
249    /// Passing a closure that contains a captured variable that needs to be traced by the garbage
250    /// collector could cause an use after free, memory corruption or other kinds of **Undefined
251    /// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
252    /// on why that is the case.
253    pub unsafe fn from_closure<F>(closure: F) -> Self
254    where
255        F: Fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue> + 'static,
256    {
257        // SAFETY: The caller must ensure the invariants of the closure hold.
258        unsafe {
259            Self::from_closure_with_captures(
260                move |this, args, (), context| closure(this, args, context),
261                (),
262            )
263        }
264    }
265
266    /// Create a new `NativeFunction` from a closure and a list of traceable captures.
267    ///
268    /// # Safety
269    ///
270    /// Passing a closure that contains a captured variable that needs to be traced by the garbage
271    /// collector could cause an use after free, memory corruption or other kinds of **Undefined
272    /// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
273    /// on why that is the case.
274    pub unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self
275    where
276        F: Fn(&JsValue, &[JsValue], &T, &mut Context) -> JsResult<JsValue> + 'static,
277        T: Trace + 'static,
278    {
279        // Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the
280        // future: https://github.com/rust-lang/rust/issues/18598
281        let ptr = Gc::into_raw(Gc::new(Closure {
282            f: closure,
283            captures,
284        }));
285        // SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object,
286        // meaning this is safe.
287        unsafe {
288            Self {
289                inner: Inner::Closure(Gc::from_raw(ptr)),
290            }
291        }
292    }
293
294    /// Calls this `NativeFunction`, forwarding the arguments to the corresponding function.
295    #[inline]
296    pub fn call(
297        &self,
298        this: &JsValue,
299        args: &[JsValue],
300        context: &mut Context,
301    ) -> JsResult<JsValue> {
302        match self.inner {
303            Inner::PointerFn(f) => f(this, args, context),
304            Inner::Closure(ref c) => c.call(this, args, context),
305        }
306    }
307
308    /// Converts this `NativeFunction` into a `JsFunction` without setting its name or length.
309    ///
310    /// Useful to create functions that will only be used once, such as callbacks.
311    #[must_use]
312    pub fn to_js_function(self, realm: &Realm) -> JsFunction {
313        FunctionObjectBuilder::new(realm, self).build()
314    }
315}
316
317/// Call this object.
318///
319/// # Panics
320///
321/// Panics if the object is currently mutably borrowed.
322// <https://tc39.es/ecma262/#sec-built-in-function-objects-call-thisargument-argumentslist>
323pub(crate) fn native_function_call(
324    obj: &JsObject,
325    argument_count: usize,
326    context: &mut InternalMethodCallContext<'_>,
327) -> JsResult<CallValue> {
328    let args = context
329        .vm
330        .stack
331        .calling_convention_pop_arguments(argument_count);
332    let _func = context.vm.stack.pop();
333    let this = context.vm.stack.pop();
334
335    // We technically don't need this since native functions don't push any new frames to the
336    // vm, but we'll eventually have to combine the native stack with the vm stack.
337    context.check_runtime_limits()?;
338    let this_function_object = obj.clone();
339
340    let NativeFunctionObject {
341        f: function,
342        name,
343        constructor,
344        realm,
345    } = obj
346        .downcast_ref::<NativeFunctionObject>()
347        .expect("the object should be a native function object")
348        .clone();
349
350    let pc = context.vm.frame().pc;
351    let native_source_info = context.native_source_info();
352    context
353        .vm
354        .shadow_stack
355        .push_native(pc, name, native_source_info);
356
357    let mut realm = realm.unwrap_or_else(|| context.realm().clone());
358
359    context.swap_realm(&mut realm);
360    context.vm.native_active_function = Some(this_function_object);
361
362    let result = if constructor.is_some() {
363        function.call(&JsValue::undefined(), &args, context)
364    } else {
365        function.call(&this, &args, context)
366    }
367    .map_err(|err| err.inject_realm(context.realm().clone()));
368
369    context.vm.native_active_function = None;
370    context.swap_realm(&mut realm);
371
372    context.vm.shadow_stack.pop();
373
374    context.vm.stack.push(result?);
375
376    Ok(CallValue::Complete)
377}
378
379/// Construct an instance of this object with the specified arguments.
380///
381/// # Panics
382///
383/// Panics if the object is currently mutably borrowed.
384// <https://tc39.es/ecma262/#sec-built-in-function-objects-construct-argumentslist-newtarget>
385fn native_function_construct(
386    obj: &JsObject,
387    argument_count: usize,
388    context: &mut InternalMethodCallContext<'_>,
389) -> JsResult<CallValue> {
390    // We technically don't need this since native functions don't push any new frames to the
391    // vm, but we'll eventually have to combine the native stack with the vm stack.
392    context.check_runtime_limits()?;
393    let this_function_object = obj.clone();
394
395    let NativeFunctionObject {
396        f: function,
397        name,
398        constructor,
399        realm,
400    } = obj
401        .downcast_ref::<NativeFunctionObject>()
402        .expect("the object should be a native function object")
403        .clone();
404
405    let pc = context.vm.frame().pc;
406    let native_source_info = context.native_source_info();
407    context
408        .vm
409        .shadow_stack
410        .push_native(pc, name, native_source_info);
411
412    let mut realm = realm.unwrap_or_else(|| context.realm().clone());
413
414    context.swap_realm(&mut realm);
415    context.vm.native_active_function = Some(this_function_object);
416
417    let new_target = context.vm.stack.pop();
418    let args = context
419        .vm
420        .stack
421        .calling_convention_pop_arguments(argument_count);
422    let _func = context.vm.stack.pop();
423    let _this = context.vm.stack.pop();
424
425    let result = function
426        .call(&new_target, &args, context)
427        .map_err(|err| err.inject_realm(context.realm().clone()))
428        .and_then(|v| match v.variant() {
429            JsVariant::Object(o) => Ok(o.clone()),
430            val => {
431                if constructor.expect("must be a constructor").is_base() || val.is_undefined() {
432                    let prototype = get_prototype_from_constructor(
433                        &new_target,
434                        StandardConstructors::object,
435                        context,
436                    )?;
437                    Ok(JsObject::from_proto_and_data_with_shared_shape(
438                        context.root_shape(),
439                        prototype,
440                        OrdinaryObject,
441                    )
442                    .upcast())
443                } else {
444                    Err(JsNativeError::typ()
445                        .with_message("derived constructor can only return an Object or undefined")
446                        .into())
447                }
448            }
449        });
450
451    context.vm.native_active_function = None;
452    context.swap_realm(&mut realm);
453
454    context.vm.shadow_stack.pop();
455
456    context.vm.stack.push(result?);
457
458    Ok(CallValue::Complete)
459}