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}