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..77858bd3d --- /dev/null +++ b/crates/neon-macros/src/export/function/meta.rs @@ -0,0 +1,82 @@ +#[derive(Default)] +pub(crate) struct Meta { + pub(super) kind: Kind, + pub(super) name: Option, + pub(super) json: bool, + pub(super) context: 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 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("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..61cd6b0b9 --- /dev/null +++ b/crates/neon-macros/src/export/function/mod.rs @@ -0,0 +1,188 @@ +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 = meta.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,)); + + // 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 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(&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);)) + }); + + // 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 = cx + .task(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_ident(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 = match path.path.segments.last() { + Some(path) => path, + None => return None, + }; + + 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 { + // Return early if no arguments + let first = match first_arg_ident(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(ret: &syn::ReturnType) -> bool { + 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/mod.rs b/crates/neon-macros/src/export/mod.rs index 9f56e344b..021fa6cf0 100644 --- a/crates/neon-macros/src/export/mod.rs +++ b/crates/neon-macros/src/export/mod.rs @@ -1,3 +1,4 @@ +mod function; mod global; // N.B.: Meta attribute parsing happens in this function because `syn::parse_macro_input!` @@ -10,6 +11,13 @@ pub(crate) fn 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); @@ -36,7 +44,7 @@ pub(crate) fn export( // 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 consts, and statics."; + 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 4075e7047..31a0e5a73 100644 --- a/crates/neon-macros/src/lib.rs +++ b/crates/neon-macros/src/lib.rs @@ -93,6 +93,91 @@ pub fn main( /// #[neon::export] /// static MESSAGES: &[&str] = &["hello", "goodbye"]; /// ``` +/// +/// ## Exporting functions +/// +/// Functions may take any type that implements [`neon::types::extract::TryFromJs`] as +/// an argument and return any type that implements [`neon::types::extract::TryIntoJs`]. +/// +/// ```ignore +/// #[neon::export] +/// fn add(a: f64, b: f64) -> f64 { +/// a + b +/// } +/// ``` +/// +/// ### Interact with the JavaScript runtime +/// +/// More complex functions may need to interact directly with the JavaScript runtime, +/// for example with [`neon::context::Context`] or handles to JavaScript values. +/// +/// Functions may optionally include a [`neon::context::FunctionContext`] argument. Note +/// that unlike functions created with [`neon::types::JsFunction::new`], exported function +/// receive a borrowed context and may require explicit lifetimes. +/// +/// ```ignore +/// #[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)) +/// } +/// ``` +/// +/// ### Exporting a function that uses JSON +/// +/// The [`neon::types::extract::Json`] wrapper allows ergonomically handling complex +/// types that implement `serde::Deserialize` and `serde::Serialize`. +/// +/// ```ignore +/// #[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`. +/// +/// ```ignore +/// #[neon::export] +/// 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. +/// +/// ```ignore +/// #[neon::export] +/// fn add<'cx>(cx: 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. +/// +/// ```ignore +/// #[neon::export(task)] +/// fn add(a: f64, b: f64) -> f64 { +/// a + b +/// } +/// ``` pub fn export( attr: proc_macro::TokenStream, item: proc_macro::TokenStream,