use proc_macro::TokenStream;
use proc_macro2::{Delimiter, TokenStream as TokenStream2, TokenTree};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{format_ident, quote};
use syn::{spanned::Spanned, Error, Ident, Item, Meta, NestedMeta};
pub(crate) struct MargsSignature {
pub args: Vec<TokenStream2>,
pub ret: TokenStream2,
}
fn split_top_level_commas(tokens: TokenStream2) -> Vec<TokenStream2> {
let mut segments: Vec<TokenStream2> = vec![TokenStream2::new()];
for tt in tokens {
match &tt {
TokenTree::Punct(p) if p.as_char() == ',' => {
segments.push(TokenStream2::new())
},
_ => segments.last_mut().unwrap().extend(std::iter::once(tt)),
}
}
if segments.last().is_some_and(TokenStream2::is_empty) {
segments.pop();
}
segments
}
pub(crate) fn extract_args_ret_tokens(
attrs: TokenStream2,
) -> Result<(TokenStream2, Option<TokenStream2>, Option<TokenStream2>), Error> {
let mut passthrough: Vec<TokenStream2> = Vec::new();
let mut args_tokens: Option<TokenStream2> = None;
let mut ret_tokens: Option<TokenStream2> = None;
for segment in split_top_level_commas(attrs) {
let mut iter = segment.clone().into_iter();
let key_and_eq = match (iter.next(), iter.next()) {
(Some(TokenTree::Ident(key)), Some(TokenTree::Punct(eq)))
if eq.as_char() == '=' =>
{
Some(key)
},
_ => None,
};
let Some(key) = key_and_eq else {
passthrough.push(segment);
continue;
};
let key_name = key.to_string();
if key_name != "args" && key_name != "ret" {
passthrough.push(segment);
continue;
}
let slot = if key_name == "args" {
&mut args_tokens
} else {
&mut ret_tokens
};
if slot.is_some() {
return Err(Error::new(
key.span(),
format!("duplicate `{key_name}` export attribute argument"),
));
}
let value: TokenStream2 = iter.collect();
if value.is_empty() {
return Err(Error::new(
key.span(),
format!("expected an expression after `{key_name} =`"),
));
}
*slot = Some(value);
}
let mut remaining = TokenStream2::new();
for (i, segment) in passthrough.into_iter().enumerate() {
if i > 0 {
remaining.extend(std::iter::once(TokenTree::Punct(proc_macro2::Punct::new(
',',
proc_macro2::Spacing::Alone,
))));
}
remaining.extend(segment);
}
Ok((remaining, args_tokens, ret_tokens))
}
pub(crate) fn parse_margs_signature(
mode: Mode,
args_tokens: Option<TokenStream2>,
ret_tokens: Option<TokenStream2>,
) -> Result<Option<MargsSignature>, Error> {
if mode != Mode::Margs {
if let Some(bad) = args_tokens.or(ret_tokens) {
return Err(Error::new_spanned(
bad,
"`args`/`ret` are only valid with `#[export(margs)]` — every \
other mode gets its signature from Rust types (`FromArg`/`IntoArg`) \
or has a fixed wire shape",
));
}
return Ok(None);
}
let (args_tokens, ret_tokens) = match (args_tokens, ret_tokens) {
(Some(a), Some(r)) => (a, r),
(None, None) => return Ok(None),
(a, r) => {
return Err(Error::new_spanned(
a.or(r).unwrap(),
"`#[export(margs, ...)]` requires both `args` and `ret` together, or neither",
));
},
};
let mut iter = args_tokens.clone().into_iter();
let group = match (iter.next(), iter.next()) {
(Some(TokenTree::Group(g)), None) if g.delimiter() == Delimiter::Parenthesis => g,
_ => {
return Err(Error::new_spanned(
args_tokens,
"expected `args = (<type>, <type>, ..)` — a parenthesized, \
comma-separated list of `expr!`-style argument type specs, one \
per parameter",
));
},
};
let args = split_top_level_commas(group.stream());
Ok(Some(MargsSignature {
args,
ret: ret_tokens,
}))
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) enum Mode {
Native,
Margs,
Wstp,
Wxf,
}
pub(crate) struct Prefix {
pub crate_path: proc_macro2::TokenStream,
}
impl Prefix {
pub fn resolve() -> Self {
if let Some(tokens) = found_as("wolfram-export") {
return Self { crate_path: tokens };
}
if let Some(tokens) = found_as("wolfram-library-link") {
return Self { crate_path: tokens };
}
Self {
crate_path: quote! { ::__wolfram_export_or_wolfram_library_link_must_be_a_dependency },
}
}
}
fn found_as(name: &str) -> Option<TokenStream2> {
let renamed = match crate_name(name).ok()? {
FoundCrate::Itself => name.replace('-', "_"),
FoundCrate::Name(n) => n,
};
let ident = format_ident!("{}", renamed);
Some(quote! { ::#ident })
}
fn assert_fn_ident(mode: Mode) -> Ident {
match mode {
Mode::Native => format_ident!("__assert_native_enabled"),
Mode::Margs => format_ident!("__assert_margs_enabled"),
Mode::Wstp => format_ident!("__assert_wstp_enabled"),
Mode::Wxf => format_ident!("__assert_wxf_enabled"),
}
}
pub(crate) fn detect_mode_from_args(attrs: &syn::AttributeArgs) -> Mode {
for attr in attrs {
if let NestedMeta::Meta(Meta::Path(path)) = attr {
if path.is_ident("margs") {
return Mode::Margs;
}
if path.is_ident("wstp") {
return Mode::Wstp;
}
if path.is_ident("wxf") {
return Mode::Wxf;
}
}
}
Mode::Native
}
pub(crate) fn strip_wstp_arg(attrs: syn::AttributeArgs) -> syn::AttributeArgs {
attrs
.into_iter()
.filter(|attr| match attr {
NestedMeta::Meta(Meta::Path(path)) => {
!path.is_ident("margs") && !path.is_ident("wstp") && !path.is_ident("wxf")
},
_ => true,
})
.collect()
}
pub(crate) fn export(
mode: Mode,
attrs: syn::AttributeArgs,
item: TokenStream,
margs_signature: Option<MargsSignature>,
) -> Result<TokenStream2, Error> {
let prefix = &Prefix::resolve();
let ExportArgs {
exported_name,
hidden,
} = parse_export_attribute_args(attrs)?;
let item: Item = syn::parse(item)?;
let func = match item {
Item::Fn(func) => func,
_ => {
return Err(Error::new(
proc_macro2::Span::call_site(),
"this attribute can only be applied to `fn(..) {..}` items",
));
},
};
if let Some(async_) = func.sig.asyncness {
return Err(Error::new(
async_.span(),
"exported function cannot be `async`",
));
}
if let Some(lt) = func.sig.generics.lt_token {
return Err(Error::new(lt.span(), "exported function cannot be generic"));
}
let name = func.sig.ident.clone();
let exported_name: Ident = match exported_name {
Some(name) => name,
None => func.sig.ident.clone(),
};
let params = func.sig.inputs.clone();
let wrapper = match mode {
Mode::Native => {
export_native_function(&name, &exported_name, params.len(), hidden, prefix)
},
Mode::Margs => {
export_margs_function(&name, &exported_name, hidden, prefix, margs_signature)
},
Mode::Wstp => export_wstp_function(&name, &exported_name, params, hidden, prefix),
Mode::Wxf => export_wxf_function(&name, &exported_name, params, hidden, prefix),
};
Ok(quote! {
#func
#wrapper
})
}
fn export_native_function(
name: &Ident,
exported_name: &Ident,
parameter_count: usize,
hidden: bool,
prefix: &Prefix,
) -> TokenStream2 {
let params = vec![quote! { _ }; parameter_count];
let p = &prefix.crate_path;
let assert_fn = assert_fn_ident(Mode::Native);
let mut tokens = quote! {
const _: () = #p::#assert_fn();
mod #name {
#[no_mangle]
pub unsafe extern "C" fn #exported_name(
lib: #p::sys::WolframLibraryData,
argc: #p::sys::mint,
args: *mut #p::sys::MArgument,
res: #p::sys::MArgument,
) -> std::os::raw::c_int {
let func: fn(#(#params),*) -> _ = super::#name;
#p::macro_utils::call_native_wolfram_library_function(
lib,
args,
argc,
res,
func
)
}
}
};
if !hidden && cfg!(feature = "automate-function-loading-boilerplate") {
tokens.extend(quote! {
#p::inventory::submit! {
#p::macro_utils::LibraryLinkFunction::Native {
name: stringify!(#exported_name),
signature: || {
let func: fn(#(#params),*) -> _ = #name;
let func: &dyn #p::NativeFunction<'_> = &func;
func.signature()
}
}
}
});
}
tokens
}
fn export_margs_function(
name: &Ident,
exported_name: &Ident,
hidden: bool,
prefix: &Prefix,
signature: Option<MargsSignature>,
) -> TokenStream2 {
let p = &prefix.crate_path;
let missing_signature_warning = if signature.is_none() {
quote! {
#[deprecated(note = "\
`#[export(margs)]` without `args`/`ret` defaults its LibraryFunctionLoad \
signature to `LinkObject`/`LinkObject` (the same fixed placeholder \
`#[export(wstp)]` uses) — add `args = (..)` and `ret = ..` to this \
attribute to declare the real signature")]
const __MARGS_SIGNATURE_MISSING: () = ();
const _: () = __MARGS_SIGNATURE_MISSING;
}
} else {
TokenStream2::new()
};
let assert_fn = assert_fn_ident(Mode::Margs);
let mut tokens = quote! {
const _: () = #p::#assert_fn();
mod #name {
#missing_signature_warning
#[no_mangle]
pub unsafe extern "C" fn #exported_name(
lib: #p::sys::WolframLibraryData,
argc: #p::sys::mint,
args: *mut #p::sys::MArgument,
res: #p::sys::MArgument,
) -> std::os::raw::c_int {
let func: fn(_, _) -> _ = super::#name;
#p::macro_utils::call_native_wolfram_library_function(
lib,
args,
argc,
res,
func
)
}
}
};
let signature_fn = match &signature {
Some(MargsSignature { args, ret }) => quote! {
|| (
::std::vec![ #( ::wolfram_expr::expr!(#args) ),* ],
::wolfram_expr::expr!(#ret),
)
},
None => quote! {
|| (
::std::vec![ ::wolfram_expr::expr!(::LinkObject) ],
::wolfram_expr::expr!(::LinkObject),
)
},
};
if !hidden && cfg!(feature = "automate-function-loading-boilerplate") {
tokens.extend(quote! {
#p::inventory::submit! {
#p::macro_utils::LibraryLinkFunction::Margs {
name: stringify!(#exported_name),
signature: #signature_fn,
}
}
});
}
tokens
}
fn export_wstp_function(
name: &Ident,
exported_name: &Ident,
parameter_tys: syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
hidden: bool,
prefix: &Prefix,
) -> TokenStream2 {
let p = &prefix.crate_path;
let assert_fn = assert_fn_ident(Mode::Wstp);
let mut tokens = quote! {
const _: () = #p::#assert_fn();
mod #name {
use super::*;
#[no_mangle]
pub unsafe extern "C" fn #exported_name(
lib: #p::sys::WolframLibraryData,
raw_link: #p::wstp::sys::WSLINK,
) -> std::os::raw::c_int {
let func: fn(#parameter_tys) -> _ = super::#name;
#p::macro_utils::call_wstp_wolfram_library_function(
lib,
raw_link,
func
)
}
}
};
if !hidden && cfg!(feature = "automate-function-loading-boilerplate") {
tokens.extend(quote! {
#p::inventory::submit! {
#p::macro_utils::LibraryLinkFunction::Wstp {
name: stringify!(#exported_name)
}
}
});
}
tokens
}
fn export_wxf_function(
name: &Ident,
exported_name: &Ident,
params: syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
hidden: bool,
prefix: &Prefix,
) -> TokenStream2 {
let p = &prefix.crate_path;
let n = params.len();
let n_u64 = n as u64;
let param_types: Vec<&syn::Type> = params
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(pt) => Some(&*pt.ty),
_ => None,
})
.collect();
let arg_idents: Vec<_> = (0..n).map(|i| quote::format_ident!("__arg{}", i)).collect();
let tuple_pat = match arg_idents.len() {
0 => quote! { () },
1 => {
let id = &arg_idents[0];
quote! { (#id,) }
},
_ => quote! { (#(#arg_idents),*) },
};
let from_cursor_calls: Vec<_> = param_types
.iter()
.map(|t| {
quote! { <#t as #p::macro_utils::FromWXF>::from_wxf(__c)? }
})
.collect();
let tuple_read = match from_cursor_calls.len() {
0 => quote! { () },
1 => {
let c = &from_cursor_calls[0];
quote! { (#c,) }
},
_ => quote! { (#(#from_cursor_calls),*) },
};
let assert_fn = assert_fn_ident(Mode::Wxf);
let mut tokens = quote! {
const _: () = #p::#assert_fn();
mod #name {
use super::*;
fn __wxf_bridge(__input: &#p::macro_utils::NumericArray<u8>) -> #p::macro_utils::NumericArray<u8> {
#p::macro_utils::call_and_encode_panic(|| {
let __decoded = #p::macro_utils::decode_args(__input, #n_u64, |__c| {
let #tuple_pat = #tuple_read;
let __result = super::#name(#(#arg_idents),*);
#p::macro_utils::encode_result(&__result)
});
match __decoded {
::core::result::Result::Ok(__bytes) => __bytes,
::core::result::Result::Err(__err) => {
#p::macro_utils::encode_arg_error(__err)
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn #exported_name(
lib: #p::sys::WolframLibraryData,
argc: #p::sys::mint,
args: *mut #p::sys::MArgument,
res: #p::sys::MArgument,
) -> std::os::raw::c_int {
let func: fn(_) -> _ = __wxf_bridge;
#p::macro_utils::call_wxf_wolfram_library_function(
lib,
args,
argc,
res,
func,
)
}
}
};
if !hidden && cfg!(feature = "automate-function-loading-boilerplate") {
tokens.extend(quote! {
#p::inventory::submit! {
#p::macro_utils::LibraryLinkFunction::Wxf {
name: stringify!(#exported_name),
signature: || #p::macro_utils::wxf_signature(),
}
}
});
}
tokens
}
struct ExportArgs {
exported_name: Option<Ident>,
hidden: bool,
}
fn parse_export_attribute_args(attrs: syn::AttributeArgs) -> Result<ExportArgs, Error> {
let mut hidden = false;
let mut exported_name: Option<Ident> = None;
for attr in attrs {
match attr {
NestedMeta::Meta(ref meta) => match meta {
Meta::Path(path) if path.is_ident("hidden") => {
if hidden {
return Err(Error::new(
attr.span(),
"duplicate export `hidden` attribute argument",
));
}
hidden = true;
},
Meta::List(_) | Meta::Path(_) => {
return Err(Error::new(
attr.span(),
"unrecognized export attribute argument",
));
},
Meta::NameValue(syn::MetaNameValue { path, lit, .. }) => {
if path.is_ident("name") {
if exported_name.is_some() {
return Err(Error::new(
attr.span(),
"duplicate definition for `name`",
));
}
let lit_str = match lit {
syn::Lit::Str(str) => str,
_ => {
return Err(Error::new(
lit.span(),
"expected `name = \"...\"`",
))
},
};
exported_name = Some(
lit_str
.parse::<Ident>()
.map_err(|err| Error::new(lit_str.span(), err))?,
);
} else {
return Err(Error::new(
path.span(),
"unrecognized export attribute named argument",
));
}
},
},
NestedMeta::Lit(_) => {
return Err(Error::new(
attr.span(),
"unrecognized export attribute literal argument",
));
},
}
}
Ok(ExportArgs {
exported_name,
hidden,
})
}