nodex_api/value/
class.rs

1use crate::{api, prelude::*};
2use std::{mem::MaybeUninit, os::raw::c_char};
3
4#[derive(Copy, Clone, Debug)]
5pub struct JsClass(pub(crate) JsValue);
6
7impl JsClass {
8    pub(crate) fn from_value(value: JsValue) -> JsClass {
9        JsClass(value)
10    }
11
12    /// Defines a JavaScript class, including:
13    ///
14    /// * A JavaScript constructor function that has the class name. When wrapping a corresponding C++ class, the callback passed via constructor can be used to instantiate a new C++ class instance, which can then be placed inside the JavaScript object instance being constructed using napi_wrap.
15    /// * Properties on the constructor function whose implementation can call corresponding static data properties, accessors, and methods of the C++ class (defined by property descriptors with the napi_static attribute).
16    /// * Properties on the constructor function's prototype object. When wrapping a C++ class, non-static data properties, accessors, and methods of the C++ class can be called from the static functions given in the property descriptors without the napi_static attribute after retrieving the C++ class instance placed inside the JavaScript object instance by using napi_unwrap.
17    ///
18    /// When wrapping a C++ class, the C++ constructor callback passed via constructor should
19    /// be a static method on the class that calls the actual class constructor, then wraps the
20    /// new C++ instance in a JavaScript object, and returns the wrapper object. See napi_wrap
21    /// for details.
22    ///
23    /// The JavaScript constructor function returned from napi_define_class is often saved and
24    /// used later to construct new instances of the class from native code, and/or to check
25    /// whether provided values are instances of the class. In that case, to prevent the function
26    /// value from being garbage-collected, a strong persistent reference to it can be created
27    /// using napi_create_reference, ensuring that the reference count is kept >= 1.
28    ///
29    /// Any non-NULL data which is passed to this API via the data parameter or via the data field
30    /// of the napi_property_descriptor array items can be associated with the resulting JavaScript
31    /// constructor (which is returned in the result parameter) and freed whenever the class is
32    /// garbage-collected by passing both the JavaScript function and the data to napi_add_finalizer.
33    #[allow(clippy::type_complexity)]
34    pub fn new<F, P, T, R>(
35        env: NapiEnv,
36        name: impl AsRef<str>,
37        func: F,
38        properties: P,
39    ) -> NapiResult<JsClass>
40    where
41        T: FromJsArgs,
42        R: NapiValueT,
43        F: FnMut(JsObject, T) -> NapiResult<R>,
44        P: AsRef<[NapiPropertyDescriptor]>,
45    {
46        // NB: leak the func closure
47        let func: Box<Box<dyn FnMut(JsObject, T) -> NapiResult<R>>> = Box::new(Box::new(func));
48
49        // TODO: it just works but not very useful by current design
50        // use the trampoline function to call into the closure
51        extern "C" fn trampoline<T: FromJsArgs, R: NapiValueT>(
52            env: NapiEnv,
53            info: napi_callback_info,
54        ) -> napi_value {
55            let mut data = MaybeUninit::uninit();
56            let mut this = MaybeUninit::uninit();
57
58            let (argc, argv, this, mut func) = unsafe {
59                let mut argc = T::len();
60                let mut argv = vec![std::ptr::null_mut(); T::len()];
61
62                let status = api::napi_get_cb_info(
63                    env,
64                    info,
65                    &mut argc,
66                    argv.as_mut_ptr(),
67                    this.as_mut_ptr(),
68                    data.as_mut_ptr(),
69                );
70
71                // NB: the JsFunction maybe called multiple times, so we can should leak the
72                // closure memory here.
73                //
74                // With napi >= 5, we can add a finalizer to this function.
75                let func: &mut Box<dyn FnMut(JsObject, T) -> NapiResult<R>> =
76                    std::mem::transmute(data);
77
78                (argc, argv, this.assume_init(), func)
79            };
80
81            let args = argv
82                .into_iter()
83                .map(|arg| JsValue::from_raw(env, arg))
84                .collect();
85            let this = JsObject::from_raw(env, this);
86
87            if let Ok(args) = T::from_js_args(JsArgs(args)) {
88                napi_r!(env, =func(this, args))
89            } else {
90                env.throw_error("wrong argument type!");
91                env.undefined().unwrap().raw()
92            }
93        }
94
95        let fn_pointer = Box::into_raw(func) as DataPointer;
96        let value = napi_call!(
97            =napi_define_class,
98            env,
99            name.as_ref().as_ptr() as CharPointer,
100            name.as_ref().len(),
101            Some(trampoline::<T, R>),
102            fn_pointer,
103            properties.as_ref().len(),
104            properties.as_ref().as_ptr() as *const _,
105        );
106
107        let mut class = JsClass(JsValue(env, value));
108
109        class.gc(move |_| unsafe {
110            // NB: the leaked data is collected here.
111            let _: Box<Box<dyn FnMut(JsObject, T) -> NapiResult<R>>> =
112                Box::from_raw(fn_pointer as _);
113            Ok(())
114        })?;
115
116        Ok(class)
117    }
118
119    /// This method is used to instantiate a new JavaScript value using a given napi_value
120    /// that represents the constructor for the object.
121    pub fn new_instance<T>(&self, args: T) -> NapiResult<JsObject>
122    where
123        T: ToJsArgs,
124    {
125        let args = args
126            .to_js_args(self.env())?
127            .0
128            .into_iter()
129            .map(|value| value.raw())
130            .collect::<Vec<_>>();
131
132        let instance = napi_call!(
133            =napi_new_instance,
134            self.env(),
135            self.raw(),
136            T::len(),
137            args.as_ptr(),
138        );
139        Ok(JsObject::from_raw(self.env(), instance))
140    }
141}
142
143napi_value_t!(JsClass);
144
145impl NapiValueCheck for JsClass {
146    fn check(&self) -> NapiResult<bool> {
147        Ok(self.kind()? == NapiValuetype::Function)
148    }
149}