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
17 changes: 9 additions & 8 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/neon-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ edition = "2021"
proc-macro = true

[dependencies]
proc-macro2 = "1.0.79"
quote = "1.0.33"
syn = "2.0.39"
syn = { version = "2.0.57", features = ["full"] }
34 changes: 34 additions & 0 deletions crates/neon-macros/src/export/global/meta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#[derive(Default)]
pub(crate) struct Meta {
pub(super) name: Option<syn::LitStr>,
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<Self::Output> {
let mut attr = Meta::default();
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("name") {
attr.name = Some(meta.value()?.parse::<syn::LitStr>()?);

return Ok(());
}

if meta.path.is_ident("json") {
attr.json = true;

return Ok(());
}

Err(meta.error("unsupported property"))
});

parser.parse2(tokens)?;

Ok(attr)
}
}
44 changes: 44 additions & 0 deletions crates/neon-macros/src/export/global/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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<syn::Expr>) -> Box<syn::Expr> {
// 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.
let create_fn = quote::quote!({
#[doc(hidden)]
#[neon::macro_internal::linkme::distributed_slice(neon::macro_internal::EXPORTS)]
#[linkme(crate = neon::macro_internal::linkme)]
kjvalencik marked this conversation as resolved.
Show resolved Hide resolved
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))
}
43 changes: 43 additions & 0 deletions crates/neon-macros/src/export/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 `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 consts, and statics.";
let err = syn::Error::new(span, msg);

err.into_compile_error().into()
}
41 changes: 41 additions & 0 deletions crates/neon-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! 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.
Expand Down Expand Up @@ -58,3 +60,42 @@ pub fn main(
)
.into()
}

#[proc_macro_attribute]
/// Register an item to be exported by the Neon addon
///
/// ## Exporting constants and statics
///
/// ```ignore
/// #[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.
///
/// ```ignore
/// #[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.
///
/// ```ignore
/// #[neon::export]
/// static MESSAGES: &[&str] = &["hello", "goodbye"];
/// ```
pub fn export(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
export::export(attr, item)
}