nodex_api/value/
function.rs

1use crate::{api, prelude::*};
2use std::{marker::PhantomData, mem::MaybeUninit, os::raw::c_char};
3
4#[derive(Copy, Clone, Debug)]
5pub struct Function<F: NapiValueT>(pub(crate) JsValue, PhantomData<F>);
6
7impl<F: NapiValueT> Function<F> {
8    pub(crate) fn from_value(value: JsValue) -> Function<F> {
9        Function::<F>(value, PhantomData)
10    }
11
12    /// This API allows an add-on author to create a function object in native code.
13    /// This is the primary mechanism to allow calling into the add-on's native code
14    /// from JavaScript.
15    /// The newly created function is not automatically visible from script after this call.
16    /// Instead, a property must be explicitly set on any object that is visible to JavaScript,
17    /// in order for the function to be accessible from script.
18    /// In order to expose a function as part of the add-on's module exports, set the newly
19    /// created function on the exports object. A sample module might look as follows:
20    ///
21    /// ```c
22    /// napi_value SayHello(napi_env env, napi_callback_info info) {
23    ///  printf("Hello\n");
24    ///  return NULL;
25    /// }
26    ///
27    /// napi_value Init(napi_env env, napi_value exports) {
28    ///  napi_status status;
29    ///  napi_value fn;
30    ///  status = napi_create_function(env, NULL, 0, SayHello, NULL, &fn);
31    ///  if (status != napi_ok) return NULL;
32    ///  status = napi_set_named_property(env, exports, "sayHello", fn);
33    ///  if (status != napi_ok) return NULL;
34    ///  return exports;
35    /// }
36    ///
37    /// NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
38    /// ```
39    ///
40    /// Given the above code, the add-on can be used from JavaScript as follows:
41    ///
42    /// ```c
43    /// const myaddon = require('./addon');
44    /// myaddon.sayHello();
45    /// ```
46    ///
47    /// The string passed to require() is the name of the target in binding.gyp responsible
48    /// for creating the .node file.
49    ///
50    /// Any non-NULL data which is passed to this API via the data parameter can be associated
51    /// with the resulting JavaScript function (which is returned in the result parameter)
52    /// and freed whenever the function is garbage-collected by passing both the JavaScript
53    /// function and the data to napi_add_finalizer.
54    ///
55    /// JavaScript Functions are described in Section 19.2 of the ECMAScript Language Specification.
56    #[allow(clippy::type_complexity)]
57    pub fn new<T, R>(
58        env: NapiEnv,
59        name: Option<impl AsRef<str>>,
60        func: impl FnMut(JsObject, T) -> NapiResult<R>,
61    ) -> NapiResult<Function<R>>
62    where
63        T: FromJsArgs,
64        R: NapiValueT,
65    {
66        let (name, len) = if let Some(name) = name {
67            (name.as_ref().as_ptr() as *const c_char, name.as_ref().len())
68        } else {
69            (std::ptr::null(), 0)
70        };
71
72        // NB: leak the func closure
73        let func: Box<Box<dyn FnMut(JsObject, T) -> NapiResult<R>>> = Box::new(Box::new(func));
74
75        extern "C" fn trampoline<T: FromJsArgs, R: NapiValueT>(
76            env: NapiEnv,
77            info: napi_callback_info,
78        ) -> napi_value {
79            let mut data = MaybeUninit::uninit();
80            let mut this = MaybeUninit::uninit();
81
82            let (argc, argv, this, mut func) = unsafe {
83                // NB: use this to get the number of arguments
84                // let mut argc = 0;
85                // let mut argv = [std::ptr::null_mut(); 0];
86                // api::napi_get_cb_info(
87                //     env,
88                //     info,
89                //     &mut argc,
90                //     argv.as_mut_ptr(),
91                //     std::ptr::null_mut(),
92                //     std::ptr::null_mut(),
93                // );
94
95                let mut argc = T::len();
96                let mut argv = vec![std::ptr::null_mut(); T::len()];
97                api::napi_get_cb_info(
98                    env,
99                    info,
100                    &mut argc,
101                    argv.as_mut_ptr(),
102                    this.as_mut_ptr(),
103                    data.as_mut_ptr(),
104                );
105
106                // NB: the Function maybe called multiple times, so we can should leak the
107                // closure memory here.
108                //
109                // With napi >= 5, we can add a finalizer to this function.
110                let func: &mut Box<dyn FnMut(JsObject, T) -> NapiResult<R>> =
111                    std::mem::transmute(data);
112
113                (argc, argv, this.assume_init(), func)
114            };
115
116            let args = argv
117                .into_iter()
118                .map(|arg| JsValue::from_raw(env, arg))
119                .collect();
120            let this = JsObject::from_raw(env, this);
121
122            if let Ok(args) = T::from_js_args(JsArgs(args)) {
123                napi_r!(env, =func(this, args))
124            } else {
125                env.throw_error("wrong argument type!").unwrap();
126                env.undefined().unwrap().raw()
127            }
128        }
129
130        let fn_pointer = Box::into_raw(func) as DataPointer;
131        let value = napi_call!(
132            =napi_create_function,
133            env,
134            name,
135            len,
136            Some(trampoline::<T, R>),
137            // pass closure to trampoline function
138            fn_pointer,
139        );
140
141        let mut func = Function::<R>(JsValue::from_raw(env, value), PhantomData);
142        func.gc(move |_| unsafe {
143            // NB: the leaked data is collected here.
144            let _: Box<Box<dyn FnMut(JsObject, T) -> NapiResult<R>>> =
145                Box::from_raw(fn_pointer as _);
146            Ok(())
147        })?;
148
149        Ok(func)
150    }
151
152    /// This method allows a JavaScript function object to be called from a native add-on. This is
153    /// the primary mechanism of calling back from the add-on's native code into JavaScript. For
154    /// the special case of calling into JavaScript after an async operation, see
155    /// napi_make_callback.
156    pub fn call<T>(&self, this: JsObject, args: T) -> NapiResult<F>
157    where
158        T: ToJsArgs,
159    {
160        let args = args
161            .to_js_args(this.env())?
162            .0
163            .into_iter()
164            .map(|value| value.raw())
165            .collect::<Vec<_>>();
166
167        let value = napi_call!(
168            =napi_call_function,
169            self.env(),
170            this.raw(),
171            self.raw(),
172            T::len(),
173            args.as_ptr(),
174        );
175
176        Ok(F::from_raw(self.env(), value))
177    }
178
179    /// This method is used to instantiate a new JavaScript value using a given napi_value
180    /// that represents the constructor for the object.
181    pub fn new_instance<T, Args>(&self, args: Args) -> NapiResult<JsObject>
182    where
183        T: NapiValueT,
184        Args: AsRef<[T]>,
185    {
186        let instance = napi_call!(
187            =napi_new_instance,
188            self.env(),
189            self.raw(),
190            args.as_ref().len(),
191            args.as_ref().as_ptr() as _,
192        );
193        Ok(JsObject::from_raw(self.env(), instance))
194    }
195}
196
197impl<F: NapiValueT> NapiValueT for Function<F> {
198    fn from_raw(env: NapiEnv, raw: napi_value) -> Function<F> {
199        Function::<F>(JsValue(env, raw), PhantomData)
200    }
201
202    fn value(&self) -> JsValue {
203        self.0
204    }
205}
206
207pub type JsFunction = Function<JsValue>;
208
209impl<F: NapiValueT> NapiValueCheck for Function<F> {
210    fn check(&self) -> NapiResult<bool> {
211        Ok(self.kind()? == NapiValuetype::Function)
212    }
213}