Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(neon-macros): Export Macro #1025

Merged
merged 11 commits into from
May 10, 2024
48 changes: 29 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/neon-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
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") {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
187 changes: 187 additions & 0 deletions crates/neon-macros/src/export/function/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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 = 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<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> {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
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() {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
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> {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
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"
}
Loading
Loading