Skip to content

Commit

Permalink
feat(neon-macros): Export functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kjvalencik committed Apr 2, 2024
1 parent f6b1218 commit 1e15767
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 1 deletion.
82 changes: 82 additions & 0 deletions crates/neon-macros/src/export/function/meta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#[derive(Default)]
pub(crate) struct Meta {
pub(super) kind: Kind,
pub(super) name: Option<syn::LitStr>,
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::<syn::LitStr>()?);

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<Self::Output> {
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)
}
}
188 changes: 188 additions & 0 deletions crates/neon-macros/src/export/function/mod.rs
Original file line number Diff line number Diff line change
@@ -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<neon::types::JsValue> {
#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<bool> {
// 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"
}
10 changes: 9 additions & 1 deletion crates/neon-macros/src/export/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod function;
mod global;

// N.B.: Meta attribute parsing happens in this function because `syn::parse_macro_input!`
Expand All @@ -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);
Expand All @@ -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()
Expand Down
85 changes: 85 additions & 0 deletions crates/neon-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsNumber>,
/// b: Handle<JsNumber>,
/// ) -> 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<Vec<String>>) -> Json<Vec<String>> {
/// 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<String>) -> Vec<String> {
/// 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,
Expand Down

0 comments on commit 1e15767

Please sign in to comment.