diff --git a/Cargo.lock b/Cargo.lock index 7350a58f5..065d7b326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.57", "which", ] @@ -306,6 +306,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "linkme" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cfee0de9bd869589fb9a015e155946d1be5ff415cb844c2caccc6cc4b5db9" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf157a4dc5a29b7b464aa8fe7edeff30076e07e13646a1c3874f58477dc99f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -360,6 +380,7 @@ dependencies = [ "getrandom", "libloading 0.8.1", "linkify", + "linkme", "neon-macros", "nodejs-sys", "once_cell", @@ -377,9 +398,9 @@ dependencies = [ name = "neon-macros" version = "1.0.0" dependencies = [ + "proc-macro2", "quote", - "syn 2.0.55", - "syn-mid", + "syn 2.0.57", ] [[package]] @@ -498,7 +519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -671,7 +692,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -716,26 +737,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "syn-mid" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.55", -] - [[package]] name = "thiserror" version = "1.0.50" @@ -753,7 +763,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] diff --git a/crates/neon-macros/Cargo.toml b/crates/neon-macros/Cargo.toml index fe6033c0f..4e8d34887 100644 --- a/crates/neon-macros/Cargo.toml +++ b/crates/neon-macros/Cargo.toml @@ -11,6 +11,6 @@ edition = "2021" proc-macro = true [dependencies] +proc-macro2 = "1.0.79" quote = "1.0.33" -syn = "2.0.39" -syn-mid = "0.6.0" +syn = { version = "2.0.57", features = ["full"] } diff --git a/crates/neon-macros/src/export/function/meta.rs b/crates/neon-macros/src/export/function/meta.rs new file mode 100644 index 000000000..ebaaf6819 --- /dev/null +++ b/crates/neon-macros/src/export/function/meta.rs @@ -0,0 +1,93 @@ +#[derive(Default)] +pub(crate) struct Meta { + pub(super) kind: Kind, + pub(super) name: Option, + pub(super) json: bool, + pub(super) context: bool, + pub(super) result: bool, +} + +#[derive(Default)] +pub(super) enum Kind { + #[default] + Normal, + Task, +} + +impl Meta { + fn set_name(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + self.name = Some(meta.value()?.parse::()?); + + Ok(()) + } + + fn force_json(&mut self, _meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + self.json = true; + + Ok(()) + } + + fn force_context(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + match self.kind { + Kind::Normal => {} + Kind::Task => return Err(meta.error(super::TASK_CX_ERROR)), + } + + self.context = true; + + Ok(()) + } + + fn force_result(&mut self, _meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + self.result = true; + + Ok(()) + } + + fn make_task(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { + if self.context { + return Err(meta.error(super::TASK_CX_ERROR)); + } + + self.kind = Kind::Task; + + Ok(()) + } +} + +pub(crate) struct Parser; + +impl syn::parse::Parser for Parser { + type Output = Meta; + + fn parse2(self, tokens: proc_macro2::TokenStream) -> syn::Result { + let mut attr = Meta::default(); + let parser = syn::meta::parser(|meta| { + if meta.path.is_ident("name") { + return attr.set_name(meta); + } + + if meta.path.is_ident("json") { + return attr.force_json(meta); + } + + if meta.path.is_ident("context") { + return attr.force_context(meta); + } + + if meta.path.is_ident("result") { + return attr.force_result(meta); + } + + if meta.path.is_ident("task") { + return attr.make_task(meta); + } + + Err(meta.error("unsupported property")) + }); + + parser.parse2(tokens)?; + + Ok(attr) + } +} diff --git a/crates/neon-macros/src/export/function/mod.rs b/crates/neon-macros/src/export/function/mod.rs new file mode 100644 index 000000000..837d21ef2 --- /dev/null +++ b/crates/neon-macros/src/export/function/mod.rs @@ -0,0 +1,193 @@ +use crate::export::function::meta::Kind; + +pub(crate) mod meta; + +static TASK_CX_ERROR: &str = "`FunctionContext` is not allowed with `task` attribute"; + +pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenStream { + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = input; + + let name = &sig.ident; + + // Name for the registered create function + let create_name = quote::format_ident!("__NEON_EXPORT_CREATE__{name}"); + + // Name for the function that is wrapped by `JsFunction`. Delegates to the original. + let wrapper_name = quote::format_ident!("__NEON_EXPORT_WRAPPER__{name}"); + + // Determine if the first argument is `FunctionContext` + let has_context = match has_context_arg(&meta, &sig) { + Ok(has_context) => has_context, + Err(err) => return err.into_compile_error().into(), + }; + + // Retain the context argument, if necessary + let context_arg = has_context.then(|| quote::quote!(&mut cx,)); + + // Generate an argument list used when calling the original function + let start = if has_context { 1 } else { 0 }; + let args = (start..sig.inputs.len()).map(|i| quote::format_ident!("a{i}")); + + // Generate the tuple fields used to destructure `cx.args()`. Wrap in `Json` if necessary. + let tuple_fields = args.clone().map(|name| { + meta.json + .then(|| quote::quote!(neon::types::extract::Json(#name))) + .unwrap_or_else(|| quote::quote!(#name)) + }); + + // If necessary, wrap the return value in `Json` before calling `TryIntoJs` + let json_return = meta.json.then(|| { + is_result_output(&meta, &sig.output) + // Use `.map(Json)` on a `Result` + .then(|| quote::quote!(let res = res.map(neon::types::extract::Json);)) + // Wrap other values with `Json(res)` + .unwrap_or_else(|| quote::quote!(let res = neon::types::extract::Json(res);)) + }); + + // Default export name as identity unless a name is provided + let export_name = meta + .name + .map(|name| quote::quote!(#name)) + .unwrap_or_else(|| quote::quote!(stringify!(#name))); + + // Generate the call to the original function + let call_body = match meta.kind { + Kind::Normal => quote::quote!( + let (#(#tuple_fields,)*) = cx.args()?; + let res = #name(#context_arg #(#args),*); + #json_return + + neon::types::extract::TryIntoJs::try_into_js(res, &mut cx) + .map(|v| neon::handle::Handle::upcast(&v)) + ), + Kind::Task => quote::quote!( + let (#(#tuple_fields,)*) = cx.args()?; + let promise = neon::context::Context::task(&mut cx, move || { + let res = #name(#context_arg #(#args),*); + #json_return + res + }) + .promise(|mut cx, res| neon::types::extract::TryIntoJs::try_into_js(res, &mut cx)); + + Ok(neon::handle::Handle::upcast(&promise)) + ), + }; + + // Generate the wrapper function + let wrapper_fn = quote::quote!( + #[doc(hidden)] + fn #wrapper_name(mut cx: neon::context::FunctionContext) -> neon::result::JsResult { + #call_body + } + ); + + // Generate the function that is registered to create the function on addon initialization. + // Braces are included to prevent names from polluting user code. + let create_fn = quote::quote!({ + #[doc(hidden)] + #[neon::macro_internal::linkme::distributed_slice(neon::macro_internal::EXPORTS)] + #[linkme(crate = neon::macro_internal::linkme)] + fn #create_name<'cx>( + cx: &mut neon::context::ModuleContext<'cx>, + ) -> neon::result::NeonResult<(&'static str, neon::handle::Handle<'cx, neon::types::JsValue>)> { + static NAME: &str = #export_name; + + #wrapper_fn + + neon::types::JsFunction::with_name(cx, NAME, #wrapper_name).map(|v| ( + NAME, + neon::handle::Handle::upcast(&v), + )) + } + }); + + // Output the original function with the generated `create_fn` inside of it + quote::quote!( + #(#attrs) * + #vis #sig { + #create_fn + #block + } + ) + .into() +} + +// Get the ident for the first argument +fn first_arg_ty(sig: &syn::Signature) -> Option<&syn::Ident> { + let arg = sig.inputs.first()?; + let ty = match arg { + syn::FnArg::Receiver(v) => &*v.ty, + syn::FnArg::Typed(v) => &*v.ty, + }; + + let ty = match ty { + syn::Type::Reference(ty) => &*ty.elem, + _ => return None, + }; + + let path = match ty { + syn::Type::Path(path) => path, + _ => return None, + }; + + let path = path.path.segments.last()?; + + Some(&path.ident) +} + +// Determine if the function has a context argument and if it is allowed +fn has_context_arg(meta: &meta::Meta, sig: &syn::Signature) -> syn::Result { + // Forced context argument + if meta.context { + return Ok(true); + } + + // Return early if no arguments + let first = match first_arg_ty(sig) { + Some(first) => first, + None => return Ok(false), + }; + + // First argument isn't context + if first != "FunctionContext" { + return Ok(false); + } + + // Context is only allowed for normal functions + match meta.kind { + Kind::Normal => {} + Kind::Task => return Err(syn::Error::new(first.span(), TASK_CX_ERROR)), + } + + Ok(true) +} + +// Determine if a return type is a `Result` +fn is_result_output(meta: &meta::Meta, ret: &syn::ReturnType) -> bool { + // Forced result output + if meta.result { + return true; + } + + let ty = match ret { + syn::ReturnType::Default => return false, + syn::ReturnType::Type(_, ty) => &**ty, + }; + + let path = match ty { + syn::Type::Path(path) => path, + _ => return false, + }; + + let path = match path.path.segments.last() { + Some(path) => path, + None => return false, + }; + + path.ident == "Result" || path.ident == "NeonResult" || path.ident == "JsResult" +} diff --git a/crates/neon-macros/src/export/global/meta.rs b/crates/neon-macros/src/export/global/meta.rs new file mode 100644 index 000000000..91b392727 --- /dev/null +++ b/crates/neon-macros/src/export/global/meta.rs @@ -0,0 +1,34 @@ +#[derive(Default)] +pub(crate) struct Meta { + pub(super) name: Option, + pub(super) json: bool, +} + +pub(crate) struct Parser; + +impl syn::parse::Parser for Parser { + type Output = Meta; + + fn parse2(self, tokens: proc_macro2::TokenStream) -> syn::Result { + let mut attr = Meta::default(); + let parser = syn::meta::parser(|meta| { + if meta.path.is_ident("name") { + attr.name = Some(meta.value()?.parse::()?); + + return Ok(()); + } + + if meta.path.is_ident("json") { + attr.json = true; + + return Ok(()); + } + + Err(meta.error("unsupported property")) + }); + + parser.parse2(tokens)?; + + Ok(attr) + } +} diff --git a/crates/neon-macros/src/export/global/mod.rs b/crates/neon-macros/src/export/global/mod.rs new file mode 100644 index 000000000..5ded6a9d6 --- /dev/null +++ b/crates/neon-macros/src/export/global/mod.rs @@ -0,0 +1,49 @@ +pub(crate) mod meta; + +// Create a new block expression for the RHS of an assignment +pub(super) fn export(meta: meta::Meta, name: &syn::Ident, expr: Box) -> Box { + // Name for the registered create function + let create_name = quote::format_ident!("__NEON_EXPORT_CREATE__{name}"); + + // Default export name as identity unless a name is provided + let export_name = meta + .name + .map(|name| quote::quote!(#name)) + .unwrap_or_else(|| quote::quote!(stringify!(#name))); + + // If `json` is enabled, wrap the value in `Json` before `TryIntoJs` is called + let value = meta + .json + .then(|| quote::quote!(neon::types::extract::Json(&#name))) + .unwrap_or_else(|| quote::quote!(#name)); + + // Generate the function that is registered to create the global on addon initialization. + // Braces are included to prevent names from polluting user code. + // + // N.B.: The `linkme(..)` attribute informs the `distributed_slice(..)` macro where + // to find the `linkme` crate. It is re-exported from neon to avoid dependents from + // needing to adding a direct dependency on `linkme`. It is an undocumented feature. + // https://github.com/dtolnay/linkme/issues/54 + let create_fn = quote::quote!({ + #[doc(hidden)] + #[neon::macro_internal::linkme::distributed_slice(neon::macro_internal::EXPORTS)] + #[linkme(crate = neon::macro_internal::linkme)] + fn #create_name<'cx>( + cx: &mut neon::context::ModuleContext<'cx>, + ) -> neon::result::NeonResult<(&'static str, neon::handle::Handle<'cx, neon::types::JsValue>)> { + neon::types::extract::TryIntoJs::try_into_js(#value, cx).map(|v| ( + #export_name, + neon::handle::Handle::upcast(&v), + )) + } + }); + + // Create a block to hold the original expression and the registered crate function + let expr = quote::quote!({ + #create_fn + #expr + }); + + // Create an expression from the token stream + Box::new(syn::Expr::Verbatim(expr)) +} diff --git a/crates/neon-macros/src/export/mod.rs b/crates/neon-macros/src/export/mod.rs new file mode 100644 index 000000000..021fa6cf0 --- /dev/null +++ b/crates/neon-macros/src/export/mod.rs @@ -0,0 +1,51 @@ +mod function; +mod global; + +// N.B.: Meta attribute parsing happens in this function because `syn::parse_macro_input!` +// must be called from a function that returns `proc_macro::TokenStream`. +pub(crate) fn export( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + // Parse item to determine the type of export + let item = syn::parse_macro_input!(item as syn::Item); + + match item { + // Export a function + syn::Item::Fn(item) => { + let meta = syn::parse_macro_input!(attr with function::meta::Parser); + + function::export(meta, item) + } + + // Export a `const` + syn::Item::Const(mut item) => { + let meta = syn::parse_macro_input!(attr with global::meta::Parser); + + item.expr = global::export(meta, &item.ident, item.expr); + + quote::quote!(#item).into() + } + + // Export a `static` + syn::Item::Static(mut item) => { + let meta = syn::parse_macro_input!(attr with global::meta::Parser); + + item.expr = global::export(meta, &item.ident, item.expr); + + quote::quote!(#item).into() + } + + // Return an error span for all other types + _ => unsupported(item), + } +} + +// Generate an error for unsupported item types +fn unsupported(item: syn::Item) -> proc_macro::TokenStream { + let span = syn::spanned::Spanned::span(&item); + let msg = "`neon::export` can only be applied to functions, consts, and statics."; + let err = syn::Error::new(span, msg); + + err.into_compile_error().into() +} diff --git a/crates/neon-macros/src/lib.rs b/crates/neon-macros/src/lib.rs index 4063fda41..fe68c2111 100644 --- a/crates/neon-macros/src/lib.rs +++ b/crates/neon-macros/src/lib.rs @@ -1,54 +1,43 @@ //! Procedural macros supporting [Neon](https://docs.rs/neon/latest/neon/) +mod export; + #[proc_macro_attribute] -/// Marks a function as the main entry point for initialization in -/// a Neon module. -/// -/// This attribute should only be used _once_ in a module and will -/// be called each time the module is initialized in a context. -/// -/// ```ignore -/// #[neon::main] -/// fn main(mut cx: ModuleContext) -> NeonResult<()> { -/// let version = cx.string("1.0.0"); -/// -/// cx.export_value("version", version)?; -/// -/// Ok(()) -/// } -/// ``` -/// -/// If multiple functions are marked with `#[neon::main]`, there may be a compile error: -/// -/// ```sh -/// error: symbol `napi_register_module_v1` is already defined -/// ``` pub fn main( _attr: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let input = syn::parse_macro_input!(item as syn_mid::ItemFn); + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = syn::parse_macro_input!(item as syn::ItemFn); - let attrs = &input.attrs; - let vis = &input.vis; - let sig = &input.sig; - let block = &input.block; let name = &sig.ident; + let export_name = quote::format_ident!("__NEON_MAIN__{name}"); + let export_fn = quote::quote!({ + #[neon::macro_internal::linkme::distributed_slice(neon::macro_internal::MAIN)] + #[linkme(crate = neon::macro_internal::linkme)] + fn #export_name(cx: neon::context::ModuleContext) -> neon::result::NeonResult<()> { + #name(cx) + } + }); quote::quote!( #(#attrs) * #vis #sig { - #[no_mangle] - unsafe extern "C" fn napi_register_module_v1( - env: *mut std::ffi::c_void, - m: *mut std::ffi::c_void, - ) -> *mut std::ffi::c_void { - neon::macro_internal::initialize_module(env, m, #name); - m - } - + #export_fn #block } ) .into() } + +#[proc_macro_attribute] +pub fn export( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + export::export(attr, item) +} diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index 1f81a3573..d15b86396 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -24,6 +24,7 @@ nodejs-sys = "0.15.0" [dependencies] getrandom = { version = "0.2.11", optional = true } libloading = "0.8.1" +linkme = "0.3.25" semver = "1.0.20" smallvec = "1.11.2" once_cell = "1.18.0" diff --git a/crates/neon/src/context/internal.rs b/crates/neon/src/context/internal.rs index 12747fa8d..1f8f55cfc 100644 --- a/crates/neon/src/context/internal.rs +++ b/crates/neon/src/context/internal.rs @@ -50,11 +50,24 @@ pub trait ContextInternal<'a>: Sized { fn env(&self) -> Env; } -pub unsafe fn initialize_module( - env: *mut c_void, - exports: *mut c_void, - init: fn(ModuleContext) -> NeonResult<()>, -) { +fn default_main(mut cx: ModuleContext) -> NeonResult<()> { + crate::registered().export(&mut cx) +} + +fn init(cx: ModuleContext) -> NeonResult<()> { + if crate::macro_internal::MAIN.len() > 1 { + panic!("The `neon::main` macro must only be used once"); + } + + if let Some(main) = crate::macro_internal::MAIN.first() { + main(cx) + } else { + default_main(cx) + } +} + +#[no_mangle] +unsafe extern "C" fn napi_register_module_v1(env: *mut c_void, m: *mut c_void) -> *mut c_void { let env = env.cast(); sys::setup(env); @@ -64,9 +77,8 @@ pub unsafe fn initialize_module( }); let env = Env(env); - let exports = Handle::new_internal(JsObject::from_local(env, exports.cast())); + let exports = Handle::new_internal(JsObject::from_local(env, m.cast())); + let _ = ModuleContext::with(env, exports, init); - ModuleContext::with(env, exports, |cx| { - let _ = init(cx); - }); + m } diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 928ca12f5..a471b59b5 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -81,6 +81,7 @@ pub mod context; pub mod event; pub mod handle; +mod macros; pub mod meta; pub mod object; pub mod prelude; @@ -106,7 +107,9 @@ pub use types_docs::exports as types; #[doc(hidden)] pub mod macro_internal; -pub use neon_macros::*; +pub use crate::macros::*; + +use crate::{context::ModuleContext, handle::Handle, result::NeonResult, types::JsValue}; #[cfg(feature = "napi-6")] mod lifecycle; @@ -134,6 +137,64 @@ static MODULE_TAG: once_cell::sync::Lazy = once_cell::sync: crate::sys::TypeTag { lower, upper: 1 } }); +/// Values exported with [`neon::export`](export) +pub struct Exports(()); + +impl Exports { + /// Export all values exported with [`neon::export`](export) + /// + /// ```ignore + /// # use neon::prelude::*; + /// #[neon::main] + /// fn main(mut cx: ModuleContext) -> NeonResult<()> { + /// neon::registered().export(&mut cx)?; + /// Ok(()) + /// } + /// ``` + /// + /// For more control, iterate over exports. + /// + /// ```ignore + /// # use neon::prelude::*; + /// #[neon::main] + /// fn main(mut cx: ModuleContext) -> NeonResult<()> { + /// for create in neon::registered() { + /// let (name, value) = create(&mut cx)?; + /// + /// cx.export_value(name, value)?; + /// } + /// + /// Ok(()) + /// } + /// ``` + pub fn export(self, cx: &mut ModuleContext) -> NeonResult<()> { + for create in self { + let (name, value) = create(cx)?; + + cx.export_value(name, value)?; + } + + Ok(()) + } +} + +impl IntoIterator for Exports { + type Item = <::IntoIter as IntoIterator>::Item; + type IntoIter = std::slice::Iter< + 'static, + for<'cx> fn(&mut ModuleContext<'cx>) -> NeonResult<(&'static str, Handle<'cx, JsValue>)>, + >; + + fn into_iter(self) -> Self::IntoIter { + crate::macro_internal::EXPORTS.into_iter() + } +} + +/// Access values exported with [`neon::export`](export) +pub fn registered() -> Exports { + Exports(()) +} + #[test] #[ignore] fn feature_matrix() { diff --git a/crates/neon/src/macro_internal/mod.rs b/crates/neon/src/macro_internal/mod.rs index 54b2cf6c8..30b7e65e6 100644 --- a/crates/neon/src/macro_internal/mod.rs +++ b/crates/neon/src/macro_internal/mod.rs @@ -1,3 +1,13 @@ //! Internals needed by macros. These have to be exported for the macros to work -pub use crate::context::internal::initialize_module; +pub use linkme; + +use crate::{context::ModuleContext, handle::Handle, result::NeonResult, types::JsValue}; + +type Export<'cx> = (&'static str, Handle<'cx, JsValue>); + +#[linkme::distributed_slice] +pub static EXPORTS: [for<'cx> fn(&mut ModuleContext<'cx>) -> NeonResult>]; + +#[linkme::distributed_slice] +pub static MAIN: [for<'cx> fn(ModuleContext<'cx>) -> NeonResult<()>]; diff --git a/crates/neon/src/macros.rs b/crates/neon/src/macros.rs new file mode 100644 index 000000000..bab7f086a --- /dev/null +++ b/crates/neon/src/macros.rs @@ -0,0 +1,214 @@ +//! Helper module to add documentation to macros prior to re-exporting. + +/// Marks a function as the main entry point for initialization in +/// a Neon module. +/// +/// This attribute should only be used _once_ in a module and will +/// be called each time the module is initialized in a context. +/// +/// If a `main` function is not provided, all registered exports will be exported. +/// +/// ``` +/// # use neon::prelude::*; +/// # fn main() { +/// #[neon::main] +/// fn main(mut cx: ModuleContext) -> NeonResult<()> { +/// // Export all registered exports +/// neon::registered().export(&mut cx)?; +/// +/// let version = cx.string("1.0.0"); +/// +/// cx.export_value("version", version)?; +/// +/// Ok(()) +/// } +/// # } +/// ``` +pub use neon_macros::main; + +/// Register an item to be exported by the Neon addon +/// +/// ## Exporting constants and statics +/// +/// ``` +/// #[neon::export] +/// static GREETING: &str = "Hello, Neon!"; +/// +/// #[neon::export] +/// const ANSWER: u8 = 42; +/// ``` +/// +/// ### Renaming an export +/// +/// By default, items will be exported with their Rust name. Exports may +/// be renamed by providing the `name` attribute. +/// +/// ``` +/// #[neon::export(name = "myGreeting")] +/// static GREETING: &str = "Hello, Neon!"; +/// ``` +/// +/// ### JSON exports +/// +/// Complex values may be exported by automatically serializing to JSON and +/// parsing in JavaScript. Any type that implements `serde::Serialize` may be used. +/// +/// ``` +/// #[neon::export(json)] +/// static MESSAGES: &[&str] = &["hello", "goodbye"]; +/// ``` +/// +/// ## Exporting functions +/// +/// Functions may take any type that implements [`TryFromJs`](crate::types::extract::TryFromJs) as +/// an argument and return any type that implements [`TryIntoJs`](crate::types::extract::TryIntoJs). +/// +/// ``` +/// #[neon::export] +/// fn add(a: f64, b: f64) -> f64 { +/// a + b +/// } +/// ``` +/// +/// ### Exporting a function that uses JSON +/// +/// The [`Json`](crate::types::extract::Json) wrapper allows ergonomically handling complex +/// types that implement `serde::Deserialize` and `serde::Serialize`. +/// +/// ``` +/// # use neon::types::extract::Json; +/// #[neon::export] +/// fn sort(Json(mut items): Json>) -> Json> { +/// items.sort(); +/// Json(items) +/// } +/// ``` +/// +/// As a convenience, macro uses may add the `json` attribute to automatically +/// wrap arguments and return values with `Json`. +/// +/// ``` +/// #[neon::export(json)] +/// fn sort(mut items: Vec) -> Vec { +/// items.sort(); +/// items +/// } +/// ``` +/// +/// ### Tasks +/// +/// Neon provides an API for spawning tasks to execute asynchronously on Node's worker +/// pool. JavaScript may await a promise for completion of the task. +/// +/// ``` +/// # use neon::prelude::*; +/// #[neon::export] +/// fn add<'cx>(cx: &mut FunctionContext<'cx>, a: f64, b: f64) -> JsResult<'cx, JsPromise> { +/// let promise = cx +/// .task(move || a + b) +/// .promise(|mut cx, res| Ok(cx.number(res))); +/// +/// Ok(promise) +/// } +/// ``` +/// +/// As a convenience, macro users may indicate that a function should be executed +/// asynchronously on the worker pool by adding the `task` attribute. +/// +/// ``` +/// #[neon::export(task)] +/// fn add(a: f64, b: f64) -> f64 { +/// a + b +/// } +/// ``` +/// +/// ### Error Handling +/// +/// If an exported function returns a [`Result`], a JavaScript exception will be thrown +/// with the [`Err`]. Any error type that implements [`TryIntoJs`](crate::types::extract::TryIntoJs) +/// may be used. +/// +/// ``` +/// #[neon::export] +/// fn throw(msg: String) -> Result<(), String> { +/// Err(msg) +/// } +/// ``` +/// +/// The [`Error`](crate::types::extract::Error) type is provided for ergonomic error conversions +/// from most error types using the `?` operator. +/// +/// ``` +/// use neon::types::extract::Error; +/// +/// #[neon::export] +/// fn read_file(path: String) -> Result { +/// let contents = std::fs::read_to_string(path)?; +/// Ok(contents) +/// } +/// ``` +/// +/// ### Interact with the JavaScript runtime +/// +/// More complex functions may need to interact directly with the JavaScript runtime, +/// for example with [`Context`](crate::context::Context) or handles to JavaScript values. +/// +/// Functions may optionally include a [`FunctionContext`](crate::context::FunctionContext) argument. Note +/// that unlike functions created with [`JsFunction::new`](crate::types::JsFunction), exported function +/// receive a borrowed context and may require explicit lifetimes. +/// +/// ``` +/// # use neon::prelude::*; +/// #[neon::export] +/// fn add<'cx>( +/// cx: &mut FunctionContext<'cx>, +/// a: Handle, +/// b: Handle, +/// ) -> JsResult<'cx, JsNumber> { +/// let a = a.value(cx); +/// let b = b.value(cx); +/// +/// Ok(cx.number(a + b)) +/// } +/// ``` +/// +/// ### Advanced +/// +/// The following attributes are for advanced configuration and may not be +/// necessary for most users. +/// +/// #### `context` +/// +/// The `#[neon::export]` macro looks checks if the first argument has a type of +/// `&mut FunctionContext` to determine if the [`Context`](crate::context::Context) +/// should be passed to the function. +/// +/// If the type has been renamed when importing, the `context` attribute can be +/// added to force it to be passed. +/// +/// ``` +/// use neon::context::{FunctionContext as FnCtx}; +/// +/// #[neon::export(context)] +/// fn add(_cx: &mut FnCtx, a: f64, b: f64) -> f64 { +/// a + b +/// } +/// ``` +/// +/// ### `result` +/// +/// The `#[neon::export]` macro will infer an exported function returns a [`Result`] +/// if the type is named [`Result`], [`NeonResult`](crate::result::NeonResult) or +/// [`JsResult`](crate::result::JsResult). +/// +/// If a type alias is used for [`Result`], the `result` attribute can be added to +/// inform the generated code. +/// +/// ``` +/// use neon::result::{NeonResult as Res}; +/// +/// fn add(a: f64, b: f64) -> Res { +/// Ok(a + b) +/// } +/// ``` +pub use neon_macros::export; diff --git a/crates/neon/src/types_impl/extract/error.rs b/crates/neon/src/types_impl/extract/error.rs new file mode 100644 index 000000000..51b1c2105 --- /dev/null +++ b/crates/neon/src/types_impl/extract/error.rs @@ -0,0 +1,129 @@ +use std::{error, fmt}; + +use crate::{ + context::Context, + result::JsResult, + types::{extract::TryIntoJs, JsError}, +}; + +type BoxError = Box; + +#[derive(Debug)] +/// Error that implements [`TryIntoJs`] and can produce specific error types. +/// +/// [`Error`] implements [`From`] for most error types, allowing ergonomic error handling in +/// exported functions with the `?` operator. +/// +/// ### Example +/// +/// ``` +/// use neon::types::extract::Error; +/// +/// #[neon::export] +/// fn read_file(path: String) -> Result { +/// let contents = std::fs::read_to_string(path)?; +/// Ok(contents) +/// } +/// ``` +pub struct Error { + cause: BoxError, + kind: Option, +} + +#[derive(Debug)] +enum ErrorKind { + Error, + RangeError, + TypeError, +} + +impl Error { + /// Create a new [`Error`] from a `cause` + pub fn new(cause: E) -> Self + where + E: Into, + { + Self::create(ErrorKind::Error, cause) + } + + /// Create a `RangeError` + pub fn range_error(cause: E) -> Self + where + E: Into, + { + Self::create(ErrorKind::RangeError, cause) + } + + /// Create a `TypeError` + pub fn type_error(cause: E) -> Self + where + E: Into, + { + Self::create(ErrorKind::TypeError, cause) + } + + /// Check if error is a `RangeError` + pub fn is_range_error(&self) -> bool { + matches!(self.kind, Some(ErrorKind::RangeError)) + } + + /// Check if error is a `TypeError` + pub fn is_type_error(&self) -> bool { + matches!(self.kind, Some(ErrorKind::TypeError)) + } + + /// Get a reference to the underlying `cause` + pub fn cause(&self) -> &BoxError { + &self.cause + } + + /// Extract the `std::error::Error` cause + pub fn into_cause(self) -> BoxError { + self.cause + } + + fn create(kind: ErrorKind, cause: E) -> Self + where + E: Into, + { + Self { + cause: cause.into(), + kind: Some(kind), + } + } +} + +// Blanket impl allow for ergonomic `?` error handling from typical error types (including `anyhow`) +impl From for Error +where + E: Into, +{ + fn from(cause: E) -> Self { + Self::new(cause) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}: {}", self.kind, self.cause) + } +} + +// N.B.: `TryFromJs` is not included. If Neon were to add support for additional error types, +// this would be a *breaking* change. We will wait for user demand before providing this feature. +impl<'cx> TryIntoJs<'cx> for Error { + type Value = JsError; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + let message = self.cause.to_string(); + + match self.kind { + Some(ErrorKind::RangeError) => cx.range_error(message), + Some(ErrorKind::TypeError) => cx.type_error(message), + _ => cx.error(message), + } + } +} diff --git a/crates/neon/src/types_impl/extract/json.rs b/crates/neon/src/types_impl/extract/json.rs index ddf2e1408..e99628a1d 100644 --- a/crates/neon/src/types_impl/extract/json.rs +++ b/crates/neon/src/types_impl/extract/json.rs @@ -4,7 +4,7 @@ use crate::{ object::Object, result::{JsResult, NeonResult}, types::{ - extract::{private, TryFromJs}, + extract::{private, TryFromJs, TryIntoJs}, JsFunction, JsObject, JsString, JsValue, }, }; @@ -53,7 +53,44 @@ where .map(|s| s.value(cx)) } -/// Extract a value by serializing to JSON +fn global_json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + cx.global::("JSON")?.get(cx, "parse") +} + +#[cfg(not(feature = "napi-6"))] +fn json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + global_json_parse(cx) +} + +#[cfg(feature = "napi-6")] +fn json_parse<'cx, C>(cx: &mut C) -> JsResult<'cx, JsFunction> +where + C: Context<'cx>, +{ + static PARSE: LocalKey> = LocalKey::new(); + + PARSE + .get_or_try_init(cx, |cx| global_json_parse(cx).map(|f| f.root(cx))) + .map(|f| f.to_inner(cx)) +} + +fn parse<'cx, C>(cx: &mut C, s: &str) -> JsResult<'cx, JsValue> +where + C: Context<'cx>, +{ + let s = cx.string(s).upcast(); + + json_parse(cx)?.call(cx, s, [s]) +} + +/// Wrapper for converting between `T` and [`JsValue`](crate::types::JsValue) by +/// serializing with JSON. pub struct Json(pub T); impl<'cx, T> TryFromJs<'cx> for Json @@ -77,4 +114,20 @@ where } } +impl<'cx, T> TryIntoJs<'cx> for Json +where + T: serde::Serialize, +{ + type Value = JsValue; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + let s = serde_json::to_string(&self.0).or_else(|err| cx.throw_error(err.to_string()))?; + + parse(cx, &s) + } +} + impl private::Sealed for Json {} diff --git a/crates/neon/src/types_impl/extract/mod.rs b/crates/neon/src/types_impl/extract/mod.rs index 7e03f19dc..043739ca7 100644 --- a/crates/neon/src/types_impl/extract/mod.rs +++ b/crates/neon/src/types_impl/extract/mod.rs @@ -99,40 +99,105 @@ //! Note well, in this example, type annotations are not required on the tuple because //! Rust is able to infer it from the type arguments on `add` and `concat`. +use std::{fmt, marker::PhantomData}; + use crate::{ context::{Context, FunctionContext}, handle::Handle, - result::NeonResult, - types::JsValue, + result::{JsResult, NeonResult, ResultExt}, + types::{JsValue, Value}, }; +pub use self::error::Error; #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] -pub use self::json::*; -pub use self::types::*; +pub use self::json::Json; +mod error; #[cfg(feature = "serde")] mod json; -mod types; +mod private; +mod try_from_js; +mod try_into_js; /// Extract Rust data from a JavaScript value pub trait TryFromJs<'cx> where Self: private::Sealed + Sized, { + /// Error indicating non-JavaScript exception failure when extracting // Consider adding a trait bound prior to unsealing `TryFromjs` // https://github.com/neon-bindings/neon/issues/1026 type Error; + /// Extract this Rust type from a JavaScript value fn try_from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult> where C: Context<'cx>; + /// Same as [`TryFromJs`], but all errors are converted to JavaScript exceptions fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult where C: Context<'cx>; } +/// Convert Rust data into a JavaScript value +pub trait TryIntoJs<'cx> +where + Self: private::Sealed, +{ + /// The type of JavaScript value that will be created + type Value: Value; + + /// Convert `self` into a JavaScript value + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>; +} + +#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] +#[cfg(feature = "napi-5")] +/// Wrapper for converting between [`f64`] and [`JsDate`](super::JsDate) +pub struct Date(pub f64); + +/// Wrapper for converting between [`Vec`] and [`JsArrayBuffer`](super::JsArrayBuffer) +pub struct ArrayBuffer(pub Vec); + +/// Wrapper for converting between [`Vec`] and [`JsBuffer`](super::JsBuffer) +pub struct Buffer(pub Vec); + +/// Error returned when a JavaScript value is not the type expected +pub struct TypeExpected(PhantomData); + +impl TypeExpected { + fn new() -> Self { + Self(PhantomData) + } +} + +impl fmt::Display for TypeExpected { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "expected {}", T::name()) + } +} + +impl fmt::Debug for TypeExpected { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("TypeExpected").field(&T::name()).finish() + } +} + +impl std::error::Error for TypeExpected {} + +impl ResultExt for Result> { + fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult { + match self { + Ok(v) => Ok(v), + Err(_) => cx.throw_type_error(format!("expected {}", U::name())), + } + } +} + /// Trait specifying values that may be extracted from function arguments. /// /// **Note:** This trait is implemented for tuples of up to 32 values, but for @@ -221,15 +286,3 @@ from_args!( T27, T28, T29, T30, T31, T32 ] ); - -mod private { - use crate::{context::FunctionContext, result::NeonResult}; - - pub trait Sealed {} - - pub trait FromArgsInternal<'cx>: Sized { - fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult; - - fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult>; - } -} diff --git a/crates/neon/src/types_impl/extract/private.rs b/crates/neon/src/types_impl/extract/private.rs new file mode 100644 index 000000000..1226ecbee --- /dev/null +++ b/crates/neon/src/types_impl/extract/private.rs @@ -0,0 +1,72 @@ +use crate::{ + context::FunctionContext, + handle::Handle, + result::{NeonResult, Throw}, + types::{ + buffer::Binary, + extract::{ArrayBuffer, Buffer, Date, Error}, + JsTypedArray, Value, + }, +}; + +pub trait Sealed {} + +pub trait FromArgsInternal<'cx>: Sized { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult; + + fn from_args_opt(cx: &mut FunctionContext<'cx>) -> NeonResult>; +} + +macro_rules! impl_sealed { + ($ty:ident) => { + impl Sealed for $ty {} + }; + + ($($ty:ident),* $(,)*) => { + $( + impl_sealed!($ty); + )* + } +} + +impl Sealed for () {} + +impl Sealed for &str {} + +impl<'cx, V: Value> Sealed for Handle<'cx, V> {} + +impl Sealed for Option {} + +impl Sealed for Result {} + +impl Sealed for Vec +where + JsTypedArray: Value, + T: Binary, +{ +} + +impl Sealed for &[T] +where + JsTypedArray: Value, + T: Binary, +{ +} + +impl_sealed!( + u8, + u16, + u32, + i8, + i16, + i32, + f32, + f64, + bool, + String, + Date, + Buffer, + ArrayBuffer, + Throw, + Error, +); diff --git a/crates/neon/src/types_impl/extract/types.rs b/crates/neon/src/types_impl/extract/try_from_js.rs similarity index 81% rename from crates/neon/src/types_impl/extract/types.rs rename to crates/neon/src/types_impl/extract/try_from_js.rs index b8fabcfea..c2c133095 100644 --- a/crates/neon/src/types_impl/extract/types.rs +++ b/crates/neon/src/types_impl/extract/try_from_js.rs @@ -3,7 +3,7 @@ // because they can combine two Node-API calls into a single call that both // gets the value and checks the type at the same time. -use std::{convert::Infallible, error, fmt, marker::PhantomData, ptr}; +use std::{convert::Infallible, ptr}; use crate::{ context::Context, @@ -12,7 +12,7 @@ use crate::{ sys, types::{ buffer::{Binary, TypedArray}, - extract::{private, TryFromJs}, + extract::{ArrayBuffer, Buffer, Date, TryFromJs, TypeExpected}, private::ValueInternal, JsArrayBuffer, JsBoolean, JsBuffer, JsNumber, JsString, JsTypedArray, JsValue, Value, }, @@ -21,38 +21,6 @@ use crate::{ #[cfg(feature = "napi-5")] use crate::types::JsDate; -/// Error returned when a JavaScript value is not the type expected -pub struct TypeExpected(PhantomData); - -impl TypeExpected { - fn new() -> Self { - Self(PhantomData) - } -} - -impl fmt::Display for TypeExpected { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "expected {}", T::name()) - } -} - -impl fmt::Debug for TypeExpected { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("TypeExpected").field(&T::name()).finish() - } -} - -impl error::Error for TypeExpected {} - -impl ResultExt for Result> { - fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult { - match self { - Ok(v) => Ok(v), - Err(_) => cx.throw_type_error(format!("expected {}", U::name())), - } - } -} - macro_rules! from_js { () => { fn from_js(cx: &mut C, v: Handle<'cx, JsValue>) -> NeonResult @@ -80,8 +48,6 @@ where from_js!(); } -impl<'cx, V: Value> private::Sealed for Handle<'cx, V> {} - impl<'cx, T> TryFromJs<'cx> for Option where T: TryFromJs<'cx>, @@ -111,8 +77,6 @@ where } } -impl<'cx, T> private::Sealed for Option where T: TryFromJs<'cx> {} - impl<'cx> TryFromJs<'cx> for f64 { type Error = TypeExpected; @@ -136,8 +100,6 @@ impl<'cx> TryFromJs<'cx> for f64 { from_js!(); } -impl private::Sealed for f64 {} - impl<'cx> TryFromJs<'cx> for bool { type Error = TypeExpected; @@ -161,8 +123,6 @@ impl<'cx> TryFromJs<'cx> for bool { from_js!(); } -impl private::Sealed for bool {} - impl<'cx> TryFromJs<'cx> for String { type Error = TypeExpected; @@ -208,13 +168,6 @@ impl<'cx> TryFromJs<'cx> for String { from_js!(); } -impl private::Sealed for String {} - -#[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] -#[cfg(feature = "napi-5")] -/// Extract an [`f64`] from a [`JsDate`] -pub struct Date(pub f64); - #[cfg_attr(docsrs, doc(cfg(feature = "napi-5")))] #[cfg(feature = "napi-5")] impl<'cx> TryFromJs<'cx> for Date { @@ -240,8 +193,6 @@ impl<'cx> TryFromJs<'cx> for Date { from_js!(); } -impl private::Sealed for Date {} - // This implementation primarily exists for macro authors. It is infallible, rather // than checking a type, to match the JavaScript conventions of ignoring additional // arguments. @@ -273,8 +224,6 @@ impl<'cx> TryFromJs<'cx> for () { } } -impl private::Sealed for () {} - impl<'cx, T> TryFromJs<'cx> for Vec where JsTypedArray: Value, @@ -297,16 +246,6 @@ where from_js!(); } -impl private::Sealed for Vec -where - JsTypedArray: Value, - T: Binary, -{ -} - -/// Extract a [`Vec`] from a [`JsBuffer`] -pub struct Buffer(pub Vec); - impl<'cx> TryFromJs<'cx> for Buffer { type Error = TypeExpected; @@ -325,11 +264,6 @@ impl<'cx> TryFromJs<'cx> for Buffer { from_js!(); } -impl private::Sealed for Buffer {} - -/// Extract a [`Vec`] from a [`JsArrayBuffer`] -pub struct ArrayBuffer(pub Vec); - impl<'cx> TryFromJs<'cx> for ArrayBuffer { type Error = TypeExpected; @@ -348,8 +282,6 @@ impl<'cx> TryFromJs<'cx> for ArrayBuffer { from_js!(); } -impl private::Sealed for ArrayBuffer {} - fn is_null_or_undefined<'cx, C, V>(cx: &mut C, v: Handle) -> NeonResult where C: Context<'cx>, diff --git a/crates/neon/src/types_impl/extract/try_into_js.rs b/crates/neon/src/types_impl/extract/try_into_js.rs new file mode 100644 index 000000000..cd783352c --- /dev/null +++ b/crates/neon/src/types_impl/extract/try_into_js.rs @@ -0,0 +1,206 @@ +use crate::{ + context::Context, + handle::Handle, + result::{JsResult, ResultExt, Throw}, + types::{ + buffer::Binary, + extract::{ArrayBuffer, Buffer, Date, TryIntoJs}, + JsArrayBuffer, JsBoolean, JsBuffer, JsDate, JsNumber, JsString, JsTypedArray, JsUndefined, + JsValue, Value, + }, +}; + +impl<'cx, T> TryIntoJs<'cx> for Handle<'cx, T> +where + T: Value, +{ + type Value = T; + + fn try_into_js(self, _cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(self) + } +} + +impl<'cx, T, E> TryIntoJs<'cx> for Result +where + T: TryIntoJs<'cx>, + E: TryIntoJs<'cx>, +{ + type Value = T::Value; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + match self { + Ok(v) => v.try_into_js(cx), + Err(err) => { + let err = err.try_into_js(cx)?; + + cx.throw(err) + } + } + } +} + +impl<'cx> TryIntoJs<'cx> for Throw { + type Value = JsValue; + + fn try_into_js(self, _cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Err(self) + } +} + +impl<'cx, T> TryIntoJs<'cx> for Option +where + T: TryIntoJs<'cx>, +{ + type Value = JsValue; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + if let Some(val) = self { + val.try_into_js(cx).map(|v| v.upcast()) + } else { + Ok(cx.undefined().upcast()) + } + } +} + +macro_rules! impl_number { + ($ty:ident) => { + impl<'cx> TryIntoJs<'cx> for $ty { + type Value = JsNumber; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(cx.number(self)) + } + } + }; + + ($($ty:ident),* $(,)?) => { + $( + impl_number!($ty); + )* + } +} + +impl_number!(u8, u16, u32, i8, i16, i32, f32, f64); + +impl<'cx> TryIntoJs<'cx> for String { + type Value = JsString; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(cx.string(self)) + } +} + +impl<'cx> TryIntoJs<'cx> for &'cx str { + type Value = JsString; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(cx.string(self)) + } +} + +impl<'cx, T> TryIntoJs<'cx> for Vec +where + JsTypedArray: Value, + T: Binary, +{ + type Value = JsTypedArray; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + JsTypedArray::from_slice(cx, &self) + } +} + +impl<'cx, T> TryIntoJs<'cx> for &'cx [T] +where + JsTypedArray: Value, + T: Binary, +{ + type Value = JsTypedArray; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + JsTypedArray::from_slice(cx, self) + } +} + +impl<'cx> TryIntoJs<'cx> for bool { + type Value = JsBoolean; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(cx.boolean(self)) + } +} + +impl<'cx> TryIntoJs<'cx> for () { + type Value = JsUndefined; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + Ok(cx.undefined()) + } +} + +impl<'cx> TryIntoJs<'cx> for ArrayBuffer { + type Value = JsArrayBuffer; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + JsArrayBuffer::from_slice(cx, &self.0) + } +} + +impl<'cx> TryIntoJs<'cx> for Buffer { + type Value = JsBuffer; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + JsBuffer::from_slice(cx, &self.0) + } +} + +impl<'cx> TryIntoJs<'cx> for Date { + type Value = JsDate; + + fn try_into_js(self, cx: &mut C) -> JsResult<'cx, Self::Value> + where + C: Context<'cx>, + { + cx.date(self.0).or_throw(cx) + } +} diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index 73df1d312..3f8677e51 100644 --- a/crates/neon/src/types_impl/mod.rs +++ b/crates/neon/src/types_impl/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod private; pub(crate) mod utf8; use std::{ + any, fmt::{self, Debug}, os::raw::c_void, }; @@ -1063,6 +1064,7 @@ unsafe fn prepare_call<'a, 'b, C: Context<'a>>( impl JsFunction { #[cfg(not(feature = "napi-5"))] + /// Returns a new `JsFunction` implemented by `f`. pub fn new<'a, C, U>( cx: &mut C, f: fn(FunctionContext) -> JsResult, @@ -1071,7 +1073,9 @@ impl JsFunction { C: Context<'a>, U: Value, { - Self::new_internal(cx, f) + let name = any::type_name::(); + + Self::new_internal(cx, f, name) } #[cfg(feature = "napi-5")] @@ -1082,23 +1086,48 @@ impl JsFunction { F: Fn(FunctionContext) -> JsResult + 'static, V: Value, { - Self::new_internal(cx, f) + let name = any::type_name::(); + + Self::new_internal(cx, f, name) + } + + #[cfg(not(feature = "napi-5"))] + /// Returns a new `JsFunction` implemented by `f` with specified name + pub fn with_name<'a, C, U>( + cx: &mut C, + name: &str, + f: fn(FunctionContext) -> JsResult, + ) -> JsResult<'a, JsFunction> + where + C: Context<'a>, + U: Value, + { + Self::new_internal(cx, f, name) + } + + #[cfg(feature = "napi-5")] + /// Returns a new `JsFunction` implemented by `f` with specified name + pub fn with_name<'a, C, F, V>(cx: &mut C, name: &str, f: F) -> JsResult<'a, JsFunction> + where + C: Context<'a>, + F: Fn(FunctionContext) -> JsResult + 'static, + V: Value, + { + Self::new_internal(cx, f, name) } - fn new_internal<'a, C, F, V>(cx: &mut C, f: F) -> JsResult<'a, JsFunction> + fn new_internal<'a, C, F, V>(cx: &mut C, f: F, name: &str) -> JsResult<'a, JsFunction> where C: Context<'a>, F: Fn(FunctionContext) -> JsResult + 'static, V: Value, { - use std::any; use std::panic::AssertUnwindSafe; use std::ptr; use crate::context::CallbackInfo; use crate::types::error::convert_panics; - let name = any::type_name::(); let f = move |env: raw::Env, info| { let env = env.into(); let info = unsafe { CallbackInfo::new(info) }; diff --git a/test/napi/lib/export.js b/test/napi/lib/export.js new file mode 100644 index 000000000..54920dc3f --- /dev/null +++ b/test/napi/lib/export.js @@ -0,0 +1,96 @@ +const assert = require("assert"); + +const addon = require(".."); + +describe("neon::export macro", () => { + describe("globals", globals); + describe("functions", functions); +}); + +function globals() { + it("values", () => { + assert.strictEqual(addon.NUMBER, 42); + assert.strictEqual(addon.STRING, "Hello, World!"); + assert.strictEqual(addon.renamedString, "Hello, World!"); + }); + + it("json", () => { + assert.deepStrictEqual(addon.MESSAGES, ["hello", "neon"]); + assert.deepStrictEqual(addon.renamedMessages, ["hello", "neon"]); + }); +} + +function functions() { + it("void function", () => { + assert.strictEqual(addon.no_args_or_return(), undefined); + }); + + it("add - sync", () => { + assert.strictEqual(addon.simple_add(1, 2), 3); + assert.strictEqual(addon.renamedAdd(1, 2), 3); + }); + + it("add - task", async () => { + const p1 = addon.add_task(1, 2); + const p2 = addon.renamedAddTask(1, 2); + + assert.ok(p1 instanceof Promise); + assert.ok(p2 instanceof Promise); + + assert.strictEqual(await p1, 3); + assert.strictEqual(await p2, 3); + }); + + it("json sort", () => { + const arr = ["b", "c", "a"]; + const expected = [...arr].sort(); + + assert.deepStrictEqual(addon.json_sort(arr), expected); + assert.deepStrictEqual(addon.renamedJsonSort(arr), expected); + }); + + it("json sort - task", async () => { + const arr = ["b", "c", "a"]; + const expected = [...arr].sort(); + const p1 = addon.json_sort_task(arr); + const p2 = addon.renamedJsonSortTask(arr); + + assert.ok(p1 instanceof Promise); + assert.ok(p2 instanceof Promise); + + assert.deepStrictEqual(await p1, expected); + assert.deepStrictEqual(await p2, expected); + }); + + it("can use context and handles", () => { + const actual = addon.concat_with_cx_and_handle("Hello,", " World!"); + const expected = "Hello, World!"; + + assert.strictEqual(actual, expected); + }); + + it("error conversion", () => { + const msg = "Oh, no!"; + const expected = new Error(msg); + + assert.throws(() => addon.fail_with_throw(msg), expected); + }); + + it("tasks are concurrent", async () => { + const time = 500; + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const start = process.hrtime.bigint(); + + await Promise.all([addon.sleep_task(time), sleep(time)]); + + const end = process.hrtime.bigint(); + const duration = end - start; + + // If `addon.sleep_task` blocks the thread, the tasks will run sequentially + // and take a minimum of 2x `time`. Since they are run concurrently, we + // expect the time to be closer to 1x `time`. + const maxExpected = 2000000n * BigInt(time); + + assert.ok(duration < maxExpected); + }); +} diff --git a/test/napi/lib/extract.js b/test/napi/lib/extract.js index 7da6defef..7eb51a53f 100644 --- a/test/napi/lib/extract.js +++ b/test/napi/lib/extract.js @@ -9,34 +9,31 @@ describe("Extractors", () => { it("Kitchen Sink", () => { const symbol = Symbol("Test"); - - assert.deepStrictEqual( - addon.extract_values( - true, - 42, - undefined, - "hello", - new Date(0), - symbol, - 100, - "exists" - ), - [true, 42, "hello", new Date(0), symbol, 100, "exists"] - ); + const values = [ + true, + 42, + undefined, + "hello", + new Date(), + symbol, + new ArrayBuffer(100), + new Uint8Array(Buffer.from("Buffer")), + Buffer.from("Uint8Array"), + ]; // Pass `null` and `undefined` for `None` - assert.deepStrictEqual( - addon.extract_values( - true, - 42, - undefined, - "hello", - new Date(0), - symbol, - null - ), - [true, 42, "hello", new Date(0), symbol, undefined, undefined] - ); + assert.deepStrictEqual(addon.extract_values(...values, null), [ + ...values, + undefined, + undefined, + ]); + + // Pass values for optional + assert.deepStrictEqual(addon.extract_values(...values, 100, "exists"), [ + ...values, + 100, + "exists", + ]); }); it("Buffers", () => { diff --git a/test/napi/src/js/export.rs b/test/napi/src/js/export.rs new file mode 100644 index 000000000..160ef1f62 --- /dev/null +++ b/test/napi/src/js/export.rs @@ -0,0 +1,90 @@ +use neon::{prelude::*, types::extract::Error}; + +#[neon::export] +const NUMBER: u8 = 42; + +#[neon::export] +static STRING: &str = "Hello, World!"; + +#[neon::export(name = "renamedString")] +static RENAMED_STRING: &str = STRING; + +#[neon::export(json)] +static MESSAGES: &[&str] = &["hello", "neon"]; + +#[neon::export(name = "renamedMessages", json)] +static RENAMED_MESSAGES: &[&str] = MESSAGES; + +#[neon::export] +fn no_args_or_return() {} + +#[neon::export] +fn simple_add(a: f64, b: f64) -> f64 { + a + b +} + +#[neon::export(name = "renamedAdd")] +fn renamed_add(a: f64, b: f64) -> f64 { + simple_add(a, b) +} + +#[neon::export(task)] +fn add_task(a: f64, b: f64) -> f64 { + simple_add(a, b) +} + +#[neon::export(task, name = "renamedAddTask")] +fn renamed_add_task(a: f64, b: f64) -> f64 { + add_task(a, b) +} + +#[neon::export(json)] +fn json_sort(mut items: Vec) -> Vec { + items.sort(); + items +} + +#[neon::export(json, name = "renamedJsonSort")] +fn renamed_json_sort(items: Vec) -> Vec { + json_sort(items) +} + +#[neon::export(json, task)] +fn json_sort_task(items: Vec) -> Vec { + json_sort(items) +} + +#[neon::export(json, name = "renamedJsonSortTask", task)] +fn renamed_json_sort_task(items: Vec) -> Vec { + json_sort(items) +} + +#[neon::export] +fn concat_with_cx_and_handle<'cx>( + cx: &mut FunctionContext<'cx>, + a: String, + b: Handle<'cx, JsString>, +) -> Handle<'cx, JsString> { + let b = b.value(cx); + + cx.string(a + &b) +} + +#[neon::export] +fn fail_with_throw(msg: String) -> Result<(), Error> { + fn always_fails(msg: String) -> Result<(), String> { + Err(msg) + } + + // `?` converts `String` into `Error` + always_fails(msg)?; + + Ok(()) +} + +#[neon::export(task)] +fn sleep_task(ms: f64) { + use std::{thread, time::Duration}; + + thread::sleep(Duration::from_millis(ms as u64)); +} diff --git a/test/napi/src/js/extract.rs b/test/napi/src/js/extract.rs index 75759c7c2..5e0a1246c 100644 --- a/test/napi/src/js/extract.rs +++ b/test/napi/src/js/extract.rs @@ -1,38 +1,56 @@ use neon::{prelude::*, types::extract::*}; pub fn extract_values(mut cx: FunctionContext) -> JsResult { - let (boolean, number, _unit, string, Date(date), value, opt_number, opt_string): ( + #[allow(clippy::type_complexity)] + let ( + boolean, + number, + unit, + string, + Date(date), + value, + array_buf, + buf, + view, + opt_number, + opt_string, + ): ( bool, f64, (), String, Date, Handle, + ArrayBuffer, + Vec, + Buffer, Option, Option, ) = cx.args()?; + let values = [ + boolean.try_into_js(&mut cx)?.upcast(), + number.try_into_js(&mut cx)?.upcast(), + unit.try_into_js(&mut cx)?.upcast(), + string.try_into_js(&mut cx)?.upcast(), + Date(date).try_into_js(&mut cx)?.upcast(), + value, + array_buf.try_into_js(&mut cx)?.upcast(), + buf.try_into_js(&mut cx)?.upcast(), + view.try_into_js(&mut cx)?.upcast(), + opt_number + .map(|n| cx.number(n).upcast::()) + .unwrap_or_else(|| cx.undefined().upcast()), + opt_string + .map(|n| cx.string(n).upcast::()) + .unwrap_or_else(|| cx.undefined().upcast()), + ]; + let arr = cx.empty_array(); - let boolean = cx.boolean(boolean); - let number = cx.number(number); - let string = cx.string(string); - let date = cx.date(date).or_throw(&mut cx)?; - - let opt_number = opt_number - .map(|n| cx.number(n).upcast::()) - .unwrap_or_else(|| cx.undefined().upcast()); - - let opt_string = opt_string - .map(|n| cx.string(n).upcast::()) - .unwrap_or_else(|| cx.undefined().upcast()); - - arr.set(&mut cx, 0, boolean)?; - arr.set(&mut cx, 1, number)?; - arr.set(&mut cx, 2, string)?; - arr.set(&mut cx, 3, date)?; - arr.set(&mut cx, 4, value)?; - arr.set(&mut cx, 5, opt_number)?; - arr.set(&mut cx, 6, opt_string)?; + + for (i, v) in values.into_iter().enumerate() { + arr.set(&mut cx, i as u32, v)?; + } Ok(arr) } diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index 8131550f5..0065d5b77 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -12,6 +12,7 @@ mod js { pub mod coercions; pub mod date; pub mod errors; + pub mod export; pub mod extract; pub mod functions; pub mod futures; @@ -26,6 +27,10 @@ mod js { #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { + neon::registered().export(&mut cx)?; + + assert!(neon::registered().into_iter().next().is_some()); + let greeting = cx.string("Hello, World!"); let greeting_copy = greeting.value(&mut cx); let greeting_copy = cx.string(greeting_copy);