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}