quickjs_rusty/
callback.rs

1use std::ffi::{c_int, c_void};
2use std::{convert::TryFrom, marker::PhantomData, panic::RefUnwindSafe};
3
4use anyhow::Result;
5use libquickjs_ng_sys as q;
6
7use crate::utils::create_string;
8use crate::utils::create_undefined;
9use crate::ExecutionError;
10use crate::OwnedJsValue;
11use crate::ValueError;
12
13pub trait IntoCallbackResult {
14    fn into_callback_res(self, context: *mut q::JSContext) -> Result<OwnedJsValue, String>;
15}
16
17impl<T> IntoCallbackResult for T
18where
19    OwnedJsValue: From<(*mut q::JSContext, T)>,
20{
21    fn into_callback_res(self, context: *mut q::JSContext) -> Result<OwnedJsValue, String> {
22        Ok((context, self).into())
23    }
24}
25
26impl<T, E: std::fmt::Display> IntoCallbackResult for Result<T, E>
27where
28    OwnedJsValue: From<(*mut q::JSContext, T)>,
29{
30    fn into_callback_res(self, context: *mut q::JSContext) -> Result<OwnedJsValue, String> {
31        match self {
32            Ok(v) => Ok((context, v).into()),
33            Err(e) => Err(e.to_string()),
34        }
35    }
36}
37
38/// The Callback trait is implemented for functions/closures that can be
39/// used as callbacks in the JS runtime.
40pub trait Callback<F>: RefUnwindSafe {
41    /// Returns the number of required Javascript arguments.
42    fn argument_count(&self) -> usize;
43
44    /// Execute the callback.
45    ///
46    /// Should return:
47    ///   - Err(_) if the JS values could not be converted
48    ///   - Ok(Err(_)) if an error ocurred while processing.
49    ///       The given error will be raised as a JS exception.
50    ///   - Ok(Ok(result)) when execution succeeded.
51    fn call(
52        &self,
53        context: *mut q::JSContext,
54        args: Vec<OwnedJsValue>,
55    ) -> Result<Result<OwnedJsValue, String>, ValueError>;
56}
57
58macro_rules! impl_callback {
59    (@call $len:literal $self:ident $args:ident ) => {
60        $self()
61    };
62
63    (@call $len:literal $self:ident $args:ident $( $arg:ident ),* ) => {
64        {
65            let mut iter = $args.into_iter();
66            $self(
67                $(
68                    $arg::try_from(iter.next().unwrap())?,
69                )*
70            )
71        }
72    };
73
74    [ $(  $len:literal : ( $( $arg:ident, )* ), )* ] => {
75        $(
76
77            impl<
78                $( $arg, )*
79                E,
80                R,
81                F,
82            > Callback<PhantomData<(
83                $( &$arg, )*
84                &E,
85                &R,
86                &F,
87            )>> for F
88            where
89                $( $arg: TryFrom<OwnedJsValue, Error = E>, )*
90                ValueError: From<E>,
91                R: IntoCallbackResult,
92                F: Fn( $( $arg, )*  ) -> R + Sized + RefUnwindSafe,
93            {
94                fn argument_count(&self) -> usize {
95                    $len
96                }
97
98                fn call(&self, context: *mut q::JSContext, args: Vec<OwnedJsValue>)
99                    -> Result<Result<OwnedJsValue, String>, ValueError> {
100                    if args.len() != $len {
101                        return Ok(Err(format!(
102                            "Invalid argument count: Expected {}, got {}",
103                            self.argument_count(),
104                            args.len()
105                        )));
106                    }
107
108                    let res = impl_callback!(@call $len self args $($arg),* );
109                    Ok(res.into_callback_res(context))
110                }
111            }
112        )*
113    };
114}
115
116impl<R, F> Callback<PhantomData<(&R, &F)>> for F
117where
118    R: IntoCallbackResult,
119    F: Fn() -> R + Sized + RefUnwindSafe,
120{
121    fn argument_count(&self) -> usize {
122        0
123    }
124
125    fn call(
126        &self,
127        context: *mut q::JSContext,
128        args: Vec<OwnedJsValue>,
129    ) -> Result<Result<OwnedJsValue, String>, ValueError> {
130        if !args.is_empty() {
131            return Ok(Err(format!(
132                "Invalid argument count: Expected 0, got {}",
133                args.len(),
134            )));
135        }
136
137        let res = self();
138        Ok(res.into_callback_res(context))
139    }
140}
141
142impl_callback![
143    1: (A1,),
144    2: (A1, A2,),
145    3: (A1, A2, A3,),
146    4: (A1, A2, A3, A4,),
147    5: (A1, A2, A3, A4, A5,),
148];
149
150/// A wrapper around Vec<JsValue>, used for vararg callbacks.
151///
152/// To create a callback with a variable number of arguments, a callback closure
153/// must take a single `Arguments` argument.
154pub struct Arguments(Vec<OwnedJsValue>);
155
156impl Arguments {
157    /// Unpack the arguments into a Vec.
158    pub fn into_vec(self) -> Vec<OwnedJsValue> {
159        self.0
160    }
161}
162
163impl<F> Callback<PhantomData<(&Arguments, &F)>> for F
164where
165    F: Fn(Arguments) + Sized + RefUnwindSafe,
166{
167    fn argument_count(&self) -> usize {
168        0
169    }
170
171    fn call(
172        &self,
173        context: *mut q::JSContext,
174        args: Vec<OwnedJsValue>,
175    ) -> Result<Result<OwnedJsValue, String>, ValueError> {
176        (self)(Arguments(args));
177        Ok(Ok(OwnedJsValue::new(context, create_undefined())))
178    }
179}
180
181impl<F, R> Callback<PhantomData<(&Arguments, &F, &R)>> for F
182where
183    R: IntoCallbackResult,
184    F: Fn(Arguments) -> R + Sized + RefUnwindSafe,
185{
186    fn argument_count(&self) -> usize {
187        0
188    }
189
190    fn call(
191        &self,
192        context: *mut q::JSContext,
193        args: Vec<OwnedJsValue>,
194    ) -> Result<Result<OwnedJsValue, String>, ValueError> {
195        let res = (self)(Arguments(args));
196        Ok(res.into_callback_res(context))
197    }
198}
199
200/// Helper for executing a callback closure.
201pub fn exec_callback<F>(
202    context: *mut q::JSContext,
203    argc: c_int,
204    argv: *mut q::JSValue,
205    callback: &impl Callback<F>,
206) -> Result<q::JSValue, ExecutionError> {
207    let result = std::panic::catch_unwind(|| {
208        let arg_slice = unsafe { std::slice::from_raw_parts(argv, argc as usize) };
209
210        let args = arg_slice
211            .iter()
212            .map(|raw| OwnedJsValue::own(context, raw))
213            .collect::<Vec<_>>();
214
215        match callback.call(context, args) {
216            Ok(Ok(result)) => {
217                let serialized = unsafe { result.extract() };
218                Ok(serialized)
219            }
220            // TODO: better error reporting.
221            Ok(Err(e)) => Err(ExecutionError::Exception(OwnedJsValue::new(
222                context,
223                create_string(context, &e).unwrap(),
224            ))),
225            Err(e) => Err(e.into()),
226        }
227    });
228
229    match result {
230        Ok(r) => r,
231        Err(_e) => Err(ExecutionError::Internal("Callback panicked!".to_string())),
232    }
233}
234
235pub type CustomCallback = fn(*mut q::JSContext, &[q::JSValue]) -> Result<Option<q::JSValue>>;
236pub type WrappedCallback = dyn Fn(c_int, *mut q::JSValue) -> q::JSValue;
237
238/// Taken from: https://s3.amazonaws.com/temp.michaelfbryan.com/callbacks/index.html
239///
240/// Create a C wrapper function for a Rust closure to enable using it as a
241/// callback function in the Quickjs runtime.
242///
243/// Both the boxed closure and the boxed data are returned and must be stored
244/// by the caller to guarantee they stay alive.
245pub unsafe fn build_closure_trampoline<F>(
246    closure: F,
247) -> ((Box<WrappedCallback>, Box<q::JSValue>), q::JSCFunctionData)
248where
249    F: Fn(c_int, *mut q::JSValue) -> q::JSValue + 'static,
250{
251    unsafe extern "C" fn trampoline<F>(
252        _ctx: *mut q::JSContext,
253        _this: q::JSValue,
254        argc: c_int,
255        argv: *mut q::JSValue,
256        _magic: c_int,
257        data: *mut q::JSValue,
258    ) -> q::JSValue
259    where
260        F: Fn(c_int, *mut q::JSValue) -> q::JSValue,
261    {
262        let closure_ptr = q::JS_Ext_GetPtr(*data);
263        let closure: &mut F = &mut *(closure_ptr as *mut F);
264        (*closure)(argc, argv)
265    }
266
267    let boxed_f = Box::new(closure);
268
269    let data = Box::new(q::JS_Ext_NewPointer(
270        q::JS_TAG_NULL,
271        (&*boxed_f) as *const F as *mut c_void,
272    ));
273
274    ((boxed_f, data), Some(trampoline::<F>))
275}