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/// The function works directly on LibraryLink's C argument type: it receives
159/// the raw `&[MArgument]` slice and the `MArgument` return slot, exactly as
160/// the kernel passed them. `MArgument` is a union of typed pointers
161/// (`integer`, `real`, `tensor`, `sparse`, `numeric`, `utf8string`, …), so
162/// this is the escape hatch for anything plain `#[export]` can't express —
163/// types with no `FromArg`/`IntoArg` impl, like `SparseArray`, or full
164/// control over how each argument is read.
165///
166/// Reading a union field is `unsafe`: nothing checks that the field you read
167/// matches what the kernel actually sent — that's up to the signature the
168/// function is loaded with.
169///
170/// ```
171/// # mod scope {
172/// use wolfram_export::{export, sys::MArgument};
173///
174/// #[export(margs, args = (::Real, ::Real), ret = ::Real)]
175/// fn raw_add(args: &[MArgument], ret: MArgument) {
176/// unsafe { *ret.real = *args[0].real + *args[1].real; }
177/// }
178/// # }
179/// ```
180///
181/// The `FromArg`/`IntoArg` conversions that plain `#[export]` applies
182/// automatically are still there to call yourself, so only the argument that
183/// actually needs raw handling has to be done by hand:
184///
185/// ```rust,ignore
186/// use wolfram_export::{export, sys::MArgument};
187/// use wolfram_library_link::{FromArg, IntoArg, NumericArray};
188///
189/// #[export(margs,
190/// args = (::List[::LibraryDataType["NumericArray", "Real64"], "Constant"], ::Real),
191/// ret = ::LibraryDataType["NumericArray", "Real64"]
192/// )]
193/// fn scale(args: &[MArgument], ret: MArgument) {
194/// let arr = unsafe { <&NumericArray<f64>>::from_arg(&args[0]) };
195/// let factor = unsafe { f64::from_arg(&args[1]) };
196/// let scaled: Vec<f64> = arr.as_slice().iter().map(|v| v * factor).collect();
197/// unsafe { NumericArray::from_slice(&scaled).into_arg(ret) };
198/// }
199/// ```
200///
201/// For a type with no `FromArg`/`IntoArg` impl at all — reading the raw
202/// `MArgument.sparse` pointer and driving the `MSparseArray_*` C API in `rtl`
203/// directly — see `margs_sparse_array_merge` in
204/// [wolfram-examples-internal](https://github.com/WolframResearch/wolfram-rust-library/blob/master/wolfram-examples-internal/src/margs.rs).
205///
206/// The `args = (..)`/`ret = ..` annotation declares the function's
207/// `LibraryFunctionLoad` type specs — the same `{Real, Real}, Real` you would
208/// write on the Wolfram side, as `expr!` fragments (each is spliced verbatim
209/// into a `wolfram_expr::expr!` call, so `wolfram-expr` must be a direct
210/// dependency of your crate). It is not required for the function to work —
211/// you can always call `LibraryFunctionLoad` yourself with the right types —
212/// but it is what lets `cargo wl build` put a correct load call in the
213/// generated `Functions.wl`. Without it the generated entry defaults to the
214/// same fixed `LinkObject`/`LinkObject` placeholder `#[export(wstp)]` uses,
215/// which a raw `MArgument` function does *not* actually accept, and the macro
216/// emits a compile-time warning telling you to annotate it.
217///
218/// # `#[export(wstp)]` — WSTP mode
219///
220/// The function receives a `Vec<Expr>` (all arguments as a list) and returns
221/// an `Expr`, or takes a `&mut Link` for low-level control over the wire.
222/// Use this mode when the function's arguments or return value don't fit a
223/// fixed native/WXF shape — e.g. variadic arguments, or streaming a result
224/// incrementally.
225///
226/// ```
227/// # mod scope {
228/// use wolfram_export::export;
229/// use wolfram_expr::{expr, Expr, ExprKind};
230///
231/// // High-level: Vec<Expr> in, Expr out.
232/// #[export(wstp)]
233/// fn reverse(args: Vec<Expr>) -> Expr {
234/// let list = args.into_iter().next().expect("expected 1 arg");
235/// if let ExprKind::Normal(n) = list.kind() {
236/// let head = n.head().clone();
237/// expr!(head[..n.elements().iter().rev().cloned()])
238/// } else {
239/// list
240/// }
241/// }
242/// # }
243/// ```
244///
245/// # `#[export(wxf)]` — typed WXF mode
246///
247/// The generated wrapper reads a WXF-encoded `ByteArray` MArgument,
248/// deserializes all arguments via `FromWXF`, calls your function, and
249/// serializes the return value via `ToWXF` back into a `ByteArray`. Panics
250/// are caught and returned as structured `Failure["RustPanic", …]`
251/// expressions. Use this mode for structured arguments/return values
252/// (structs, enums, `Option`/`Result`) without hand-writing WSTP code.
253///
254/// ```
255/// # mod scope {
256/// use wolfram_export::export;
257/// use wolfram_expr::{expr, Expr};
258/// use wolfram_serialize::{ToWXF, FromWXF};
259///
260/// // Primitives and Vec<T> work out of the box.
261/// #[export(wxf)]
262/// fn scale(values: Vec<f64>, factor: f64) -> Vec<f64> {
263/// values.into_iter().map(|v| v * factor).collect()
264/// }
265///
266/// // `Expr` implements `ToWXF`/`FromWXF` directly (in `wolfram-expr`), so a
267/// // function can take and return untyped Wolfram Language expressions —
268/// // useful when the shape isn't known ahead of time, or a derive isn't worth
269/// // it. Build the result with the `expr!` macro.
270/// #[export(wxf)]
271/// fn add_hold(e: Expr) -> Expr {
272/// expr!(System::Hold[e])
273/// }
274///
275/// // Structs need #[derive(ToWXF, FromWXF)].
276/// #[derive(ToWXF, FromWXF)]
277/// struct Point { x: f64, y: f64 }
278///
279/// #[export(wxf)]
280/// fn midpoint(a: Point, b: Point) -> Point {
281/// Point { x: (a.x + b.x) / 2.0, y: (a.y + b.y) / 2.0 }
282/// }
283///
284/// // Option<T> and Result<T,E> are supported too.
285/// #[export(wxf)]
286/// fn safe_div(a: f64, b: f64) -> Option<f64> {
287/// if b == 0.0 { None } else { Some(a / b) }
288/// }
289/// # }
290/// ```
291///
292/// On the Wolfram side a struct maps to an `Association`:
293///
294/// ```wolfram
295/// midpoint[<|"x" -> 0.0, "y" -> 0.0|>, <|"x" -> 2.0, "y" -> 4.0|>]
296/// (* Returns <|"x" -> 1.0, "y" -> 2.0|> *)
297/// ```
298#[proc_macro_attribute]
299pub fn export(attrs: TokenStream, item: TokenStream) -> TokenStream {
300 // `args = ..`/`ret = ..` (margs-only) can't go through `syn::AttributeArgs`
301 // at all — that grammar only accepts `key = <literal>`, and these need
302 // arbitrary `expr!`-style token trees as their value. Pull them out of the
303 // raw token stream first; everything left over goes through the normal
304 // `syn::AttributeArgs`-based parser below, unchanged.
305 let (remaining, args_tokens, ret_tokens) =
306 match self::export::extract_args_ret_tokens(TokenStream2::from(attrs)) {
307 Ok(v) => v,
308 Err(err) => return err.into_compile_error().into(),
309 };
310 // `syn::AttributeArgs` (`Vec<NestedMeta>`) has no direct `Parse` impl —
311 // `parse_macro_input!(attrs as syn::AttributeArgs)` normally handles this
312 // via its own special-cased expansion; reproducing that here with an
313 // explicit `Punctuated` parser since we already hold a `TokenStream2`.
314 let attr_parser =
315 syn::punctuated::Punctuated::<syn::NestedMeta, syn::Token![,]>::parse_terminated;
316 let attrs: syn::AttributeArgs =
317 match syn::parse::Parser::parse2(attr_parser, remaining) {
318 Ok(attrs) => attrs.into_iter().collect(),
319 Err(err) => return err.into_compile_error().into(),
320 };
321 let mode = self::export::detect_mode_from_args(&attrs);
322 let attrs = self::export::strip_wstp_arg(attrs);
323
324 let margs_signature =
325 match self::export::parse_margs_signature(mode, args_tokens, ret_tokens) {
326 Ok(sig) => sig,
327 Err(err) => return err.into_compile_error().into(),
328 };
329
330 match self::export::export(mode, attrs, item, margs_signature) {
331 Ok(tokens) => tokens.into(),
332 Err(err) => err.into_compile_error().into(),
333 }
334}