nodex-api 0.2.3

rust binding to node_api.h
Documentation
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>(
        env: NapiEnv,
        name: impl AsRef<str>,
        func: F,
        properties: P,
    ) -> NapiResult<JsClass>
    where
        T: FromJsArgs,
        R: NapiValueT,
        F: FnMut(JsObject, T) -> NapiResult<R>,
        P: AsRef<[NapiPropertyDescriptor]>,
    {
        // NB: leak the func closure
        let func: Box<Box<dyn FnMut(JsObject, T) -> 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: FromJsArgs, R: NapiValueT>(
            env: NapiEnv,
            info: napi_callback_info,
        ) -> napi_value {
            let mut data = MaybeUninit::uninit();
            let mut this = MaybeUninit::uninit();

            let (argc, argv, this, mut func) = unsafe {
                let mut argc = T::len();
                let mut argv = vec![std::ptr::null_mut(); T::len()];

                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) -> NapiResult<R>> =
                    std::mem::transmute(data);

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

            let args = argv
                .into_iter()
                .map(|arg| JsValue::from_raw(env, arg))
                .collect();
            let this = JsObject::from_raw(env, this);

            if let Ok(args) = T::from_js_args(JsArgs(args)) {
                napi_r!(env, =func(this, args))
            } else {
                env.throw_error("wrong argument type!");
                env.undefined().unwrap().raw()
            }
        }

        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>),
            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) -> 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: ToJsArgs,
    {
        let args = args
            .to_js_args(self.env())?
            .0
            .into_iter()
            .map(|value| value.raw())
            .collect::<Vec<_>>();

        let instance = napi_call!(
            =napi_new_instance,
            self.env(),
            self.raw(),
            T::len(),
            args.as_ptr(),
        );
        Ok(JsObject::from_raw(self.env(), instance))
    }
}

napi_value_t!(JsClass);

impl NapiValueCheck for JsClass {
    fn check(&self) -> NapiResult<bool> {
        Ok(self.kind()? == NapiValuetype::Function)
    }
}