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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
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)
}
}