1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use crate::{api, prelude::*};
use std::{mem::MaybeUninit, os::raw::c_char};

#[derive(Copy, Clone, Debug)]
pub struct JsClass(pub(crate) JsValue);

impl JsClass {
    pub(crate) fn from_value(value: JsValue) -> JsClass {
        JsClass(value)
    }

    /// Defines a JavaScript class, including:
    ///
    /// * 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.
    /// * 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).
    /// * 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.
    ///
    /// When wrapping a C++ class, the C++ constructor callback passed via constructor should
    /// be a static method on the class that calls the actual class constructor, then wraps the
    /// new C++ instance in a JavaScript object, and returns the wrapper object. See napi_wrap
    /// for details.
    ///
    /// The JavaScript constructor function returned from napi_define_class is often saved and
    /// used later to construct new instances of the class from native code, and/or to check
    /// whether provided values are instances of the class. In that case, to prevent the function
    /// value from being garbage-collected, a strong persistent reference to it can be created
    /// using napi_create_reference, ensuring that the reference count is kept >= 1.
    ///
    /// Any non-NULL data which is passed to this API via the data parameter or via the data field
    /// of the napi_property_descriptor array items can be associated with the resulting JavaScript
    /// constructor (which is returned in the result parameter) and freed whenever the class is
    /// garbage-collected by passing both the JavaScript function and the data to napi_add_finalizer.
    #[allow(clippy::type_complexity)]
    pub fn new<F, P, T, R, const N: usize>(
        env: NapiEnv,
        name: impl AsRef<str>,
        func: F,
        properties: P,
    ) -> NapiResult<JsClass>
    where
        T: NapiValueT,
        R: NapiValueT,
        F: FnMut(JsObject, [T; N]) -> NapiResult<R>,
        P: AsRef<[NapiPropertyDescriptor]>,
    {
        // NB: leak the func closure
        let func: Box<Box<dyn FnMut(JsObject, [T; N]) -> NapiResult<R>>> = Box::new(Box::new(func));

        // TODO: it just works but not very useful by current design
        // use the trampoline function to call into the closure
        extern "C" fn trampoline<T: NapiValueT, R: NapiValueT, const N: usize>(
            env: NapiEnv,
            info: napi_callback_info,
        ) -> napi_value {
            let mut argc = N;
            let mut argv = [std::ptr::null_mut(); N];
            let mut data = MaybeUninit::uninit();
            let mut this = MaybeUninit::uninit();

            let (argc, argv, this, mut func) = unsafe {
                let status = api::napi_get_cb_info(
                    env,
                    info,
                    &mut argc,
                    argv.as_mut_ptr(),
                    this.as_mut_ptr(),
                    data.as_mut_ptr(),
                );

                // NB: the JsFunction maybe called multiple times, so we can should leak the
                // closure memory here.
                //
                // With napi >= 5, we can add a finalizer to this function.
                let func: &mut Box<dyn FnMut(JsObject, [T; N]) -> NapiResult<R>> =
                    std::mem::transmute(data);

                (argc, argv, this.assume_init(), func)
            };

            let args = unsafe { argv.map(|arg| T::from_raw(env, arg)) };
            let this = JsObject::from_raw(env, this);

            napi_r!(env, =func(this, args))
        }

        let fn_pointer = Box::into_raw(func) as DataPointer;
        let value = napi_call!(
            =napi_define_class,
            env,
            name.as_ref().as_ptr() as CharPointer,
            name.as_ref().len(),
            Some(trampoline::<T, R, N>),
            fn_pointer,
            properties.as_ref().len(),
            properties.as_ref().as_ptr() as *const _,
        );

        let mut class = JsClass(JsValue(env, value));

        class.gc(move |_| unsafe {
            // NB: the leaked data is collected here.
            let _: Box<Box<dyn FnMut(JsObject, [T; N]) -> NapiResult<R>>> =
                Box::from_raw(fn_pointer as _);
            Ok(())
        })?;

        Ok(class)
    }

    /// This method is used to instantiate a new JavaScript value using a given napi_value
    /// that represents the constructor for the object.
    pub fn new_instance<T>(&self, args: &[T]) -> NapiResult<JsObject>
    where
        T: NapiValueT,
    {
        let instance = napi_call!(
            =napi_new_instance,
            self.env(),
            self.raw(),
            args.as_ref().len(),
            args.as_ref().as_ptr() as _,
        );
        Ok(JsObject::from_raw(self.env(), instance))
    }
}

napi_value_t!(JsClass);