wolfram_export_macros/lib.rs
1//! Procedural macros for `#[export]` and `#[init]`.
2//!
3//! Emitted paths are resolved dynamically at expansion time via
4//! `proc-macro-crate`: if the caller's `Cargo.toml` has `wolfram-export` the
5//! macro emits `::wolfram_export::*`; if it has `wolfram-library-link` (legacy)
6//! it emits `::wolfram_library_link::*`. Both crates expose the same hidden
7//! runtime surface so generated code resolves correctly in both cases.
8
9mod export;
10
11use proc_macro::TokenStream;
12use proc_macro2::TokenStream as TokenStream2;
13
14use quote::quote;
15use syn::{spanned::Spanned, Error, Item};
16
17//======================================
18// #[init]
19//======================================
20
21/// Designate an initialization function to run once, when this library is
22/// loaded via Wolfram LibraryLink — distinct from [`export`], which wraps a
23/// function called on *every* invocation from Wolfram.
24///
25/// The annotated function must take no arguments and return `()`. `#[init]`
26/// can be applied to at most one function in a library, and a library isn't
27/// required to define one at all.
28///
29/// Behind the scenes, the macro generates a `WolframLibrary_initialize()` C
30/// symbol — [the well-known entry point][lib-init] the Wolfram Kernel calls
31/// automatically when the library is loaded, before any exported function
32/// runs.
33///
34/// # Panics
35///
36/// Panics inside the `#[init]` function are caught and reported to the Kernel
37/// as an error code. If initialization panics, the Kernel will not load any
38/// of this library's other exported functions.
39///
40/// # Example
41///
42/// ```
43/// use wolfram_export::init;
44///
45/// #[init]
46/// fn init_my_library() {
47/// println!("library is now initialized");
48/// }
49/// ```
50///
51/// [lib-init]: https://reference.wolfram.com/language/LibraryLink/tutorial/LibraryStructure.html#280210622
52#[proc_macro_attribute]
53pub fn init(attr: TokenStream, item: TokenStream) -> TokenStream {
54 match init_(attr.into(), item) {
55 Ok(tokens) => tokens.into(),
56 Err(err) => err.into_compile_error().into(),
57 }
58}
59
60fn init_(attr: TokenStream2, item: TokenStream) -> Result<TokenStream2, Error> {
61 if !attr.is_empty() {
62 return Err(Error::new(attr.span(), "unexpected attribute arguments"));
63 }
64
65 let item: Item = syn::parse(item)?;
66 let func = match item {
67 Item::Fn(func) => func,
68 _ => {
69 return Err(Error::new(
70 attr.span(),
71 "this attribute can only be applied to `fn(..) {..}` items",
72 ))
73 },
74 };
75
76 if let Some(async_) = func.sig.asyncness {
77 return Err(Error::new(
78 async_.span(),
79 "initialization function cannot be `async`",
80 ));
81 }
82 if let Some(lt) = func.sig.generics.lt_token {
83 return Err(Error::new(
84 lt.span(),
85 "initialization function cannot be generic",
86 ));
87 }
88 if !func.sig.inputs.is_empty() {
89 return Err(Error::new(
90 func.sig.inputs.span(),
91 "initialization function should have zero parameters",
92 ));
93 }
94
95 let user_init_fn_name: syn::Ident = func.sig.ident.clone();
96 let p = &self::export::Prefix::resolve().crate_path;
97
98 Ok(quote! {
99 #func
100
101 #[no_mangle]
102 pub unsafe extern "C" fn WolframLibrary_initialize(
103 lib: #p::sys::WolframLibraryData,
104 ) -> ::std::os::raw::c_int {
105 #p::macro_utils::init_with_user_function(
106 lib,
107 #user_init_fn_name
108 )
109 }
110 })
111}
112
113//======================================
114// #[export] — one macro, three wire formats, picked by a keyword argument.
115//======================================
116
117/// Export a function as a Wolfram LibraryLink function, in one of four wire
118/// formats picked by a keyword argument. All four need
119/// `LibraryFunctionLoad` on the Wolfram side and none of them need the
120/// `automate-function-loading-boilerplate` feature to *work* — that feature
121/// only affects whether `cargo wl build` can discover the load call for you
122/// (`#[export(margs)]` needs an `args =`/`ret =` annotation for that
123/// discovered call to be correct — see below).
124///
125/// | Attribute | Wire format | Cargo feature (on `wolfram-export`) |
126/// |-------------------|--------------------------|--------------------------------------|
127/// | `#[export]` | native `MArgument` ABI | `native` (in the default feature set) |
128/// | `#[export(margs)]`| raw `MArgument` ABI | `native` |
129/// | `#[export(wstp)]` | WSTP `LinkObject` | `wstp` |
130/// | `#[export(wxf)]` | typed WXF `ByteArray` | `wxf` |
131///
132/// ```toml
133/// # Cargo.toml
134/// wolfram-export = { version = "0.6", features = ["wstp", "wxf"] } # native is on by default
135/// ```
136///
137/// # `#[export]` — native mode (default)
138///
139/// Parameters and the return type must implement `FromArg`/`IntoArg`:
140/// `bool`, `i64`, `f64`, `String`, `NumericArray`, and references thereof.
141/// This is the fastest mode — arguments cross the LibraryLink ABI with no
142/// intermediate encoding.
143///
144/// ```
145/// # mod scope {
146/// use wolfram_export::export;
147///
148/// #[export]
149/// fn add(a: i64, b: i64) -> i64 { a + b }
150///
151/// #[export]
152/// fn greet(name: String) -> String { format!("Hello, {name}!") }
153/// # }
154/// ```
155///
156/// # `#[export(margs)]` — raw native mode
157///
158/// Same C ABI as plain `#[export]`, but the function receives the raw
159/// `&[MArgument]` arguments and `MArgument` return slot directly, with no
160/// `FromArg`/`IntoArg` marshaling performed for you. Use this when you need
161/// full manual control over argument/return conversion.
162///
163/// Since the parameter/return types aren't visible to the macro from the
164/// function signature alone, declare them by hand with `args = (..)`/
165/// `ret = ..` — each fragment is spliced verbatim into a `wolfram_expr::expr!`
166/// call, so `wolfram-expr` must be a direct dependency of your crate to use
167/// them:
168///
169/// ```
170/// # mod scope {
171/// use wolfram_export::{export, sys::MArgument};
172///
173/// #[export(margs, args = (::Real, ::Real), ret = ::Real)]
174/// fn raw_add(args: &[MArgument], ret: MArgument) {
175/// unsafe { *ret.real = *args[0].real + *args[1].real; }
176/// }
177/// # }
178/// ```
179///
180/// This makes `raw_add` show up in `cargo wl build`'s generated
181/// `Functions.wl` with a real, correct `LibraryFunctionLoad` call — same as
182/// if you'd written `LibraryFunctionLoad["...", "raw_add", {Real, Real}, Real]`
183/// by hand. Omitting `args`/`ret` still compiles, but defaults the generated
184/// entry's type spec to the same fixed `LinkObject`/`LinkObject` placeholder
185/// `#[export(wstp)]` uses — which a raw MArgument function does *not* actually
186/// accept, so calling it would misbehave — and emits a compile-time warning
187/// telling you to annotate it.
188///
189/// # `#[export(wstp)]` — WSTP mode
190///
191/// The function receives a `Vec<Expr>` (all arguments as a list) and returns
192/// an `Expr`, or takes a `&mut Link` for low-level control over the wire.
193/// Use this mode when the function's arguments or return value don't fit a
194/// fixed native/WXF shape — e.g. variadic arguments, or streaming a result
195/// incrementally.
196///
197/// ```
198/// # mod scope {
199/// use wolfram_export::export;
200/// use wolfram_expr::{expr, Expr, ExprKind};
201///
202/// // High-level: Vec<Expr> in, Expr out.
203/// #[export(wstp)]
204/// fn reverse(args: Vec<Expr>) -> Expr {
205/// let list = args.into_iter().next().expect("expected 1 arg");
206/// if let ExprKind::Normal(n) = list.kind() {
207/// let head = n.head().clone();
208/// expr!(head[..n.elements().iter().rev().cloned()])
209/// } else {
210/// list
211/// }
212/// }
213/// # }
214/// ```
215///
216/// # `#[export(wxf)]` — typed WXF mode
217///
218/// The generated wrapper reads a WXF-encoded `ByteArray` MArgument,
219/// deserializes all arguments via `FromWXF`, calls your function, and
220/// serializes the return value via `ToWXF` back into a `ByteArray`. Panics
221/// are caught and returned as structured `Failure["RustPanic", …]`
222/// expressions. Use this mode for structured arguments/return values
223/// (structs, enums, `Option`/`Result`) without hand-writing WSTP plumbing.
224///
225/// ```
226/// # mod scope {
227/// use wolfram_export::export;
228/// use wolfram_expr::{expr, Expr};
229/// use wolfram_serialize::{ToWXF, FromWXF};
230///
231/// // Primitives and Vec<T> work out of the box.
232/// #[export(wxf)]
233/// fn scale(values: Vec<f64>, factor: f64) -> Vec<f64> {
234/// values.into_iter().map(|v| v * factor).collect()
235/// }
236///
237/// // `Expr` implements `ToWXF`/`FromWXF` directly (in `wolfram-expr`), so a
238/// // function can take and return untyped Wolfram Language expressions —
239/// // useful when the shape isn't known ahead of time, or a derive isn't worth
240/// // it. Build the result with the `expr!` macro.
241/// #[export(wxf)]
242/// fn add_hold(e: Expr) -> Expr {
243/// expr!(System::Hold[e])
244/// }
245///
246/// // Structs need #[derive(ToWXF, FromWXF)].
247/// #[derive(ToWXF, FromWXF)]
248/// struct Point { x: f64, y: f64 }
249///
250/// #[export(wxf)]
251/// fn midpoint(a: Point, b: Point) -> Point {
252/// Point { x: (a.x + b.x) / 2.0, y: (a.y + b.y) / 2.0 }
253/// }
254///
255/// // Option<T> and Result<T,E> are supported too.
256/// #[export(wxf)]
257/// fn safe_div(a: f64, b: f64) -> Option<f64> {
258/// if b == 0.0 { None } else { Some(a / b) }
259/// }
260/// # }
261/// ```
262///
263/// On the Wolfram side a struct maps to an `Association`:
264///
265/// ```wolfram
266/// midpoint[<|"x" -> 0.0, "y" -> 0.0|>, <|"x" -> 2.0, "y" -> 4.0|>]
267/// (* Returns <|"x" -> 1.0, "y" -> 2.0|> *)
268/// ```
269#[proc_macro_attribute]
270pub fn export(attrs: TokenStream, item: TokenStream) -> TokenStream {
271 // `args = ..`/`ret = ..` (margs-only) can't go through `syn::AttributeArgs`
272 // at all — that grammar only accepts `key = <literal>`, and these need
273 // arbitrary `expr!`-style token trees as their value. Pull them out of the
274 // raw token stream first; everything left over goes through the normal
275 // `syn::AttributeArgs`-based parser below, unchanged.
276 let (remaining, args_tokens, ret_tokens) =
277 match self::export::extract_args_ret_tokens(TokenStream2::from(attrs)) {
278 Ok(v) => v,
279 Err(err) => return err.into_compile_error().into(),
280 };
281 // `syn::AttributeArgs` (`Vec<NestedMeta>`) has no direct `Parse` impl —
282 // `parse_macro_input!(attrs as syn::AttributeArgs)` normally handles this
283 // via its own special-cased expansion; reproducing that here with an
284 // explicit `Punctuated` parser since we already hold a `TokenStream2`.
285 let attr_parser =
286 syn::punctuated::Punctuated::<syn::NestedMeta, syn::Token![,]>::parse_terminated;
287 let attrs: syn::AttributeArgs =
288 match syn::parse::Parser::parse2(attr_parser, remaining) {
289 Ok(attrs) => attrs.into_iter().collect(),
290 Err(err) => return err.into_compile_error().into(),
291 };
292 let mode = self::export::detect_mode_from_args(&attrs);
293 let attrs = self::export::strip_wstp_arg(attrs);
294
295 let margs_signature =
296 match self::export::parse_margs_signature(mode, args_tokens, ret_tokens) {
297 Ok(sig) => sig,
298 Err(err) => return err.into_compile_error().into(),
299 };
300
301 match self::export::export(mode, attrs, item, margs_signature) {
302 Ok(tokens) => tokens.into(),
303 Err(err) => err.into_compile_error().into(),
304 }
305}