Skip to main content

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 three wire
118/// formats picked by a keyword argument. All three 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///
123/// | Attribute        | Wire format             | Cargo feature (on `wolfram-export`) |
124/// |------------------|--------------------------|--------------------------------------|
125/// | `#[export]`      | native `MArgument` ABI  | `native` (in the default feature set) |
126/// | `#[export(wstp)]`| WSTP `LinkObject`        | `wstp` |
127/// | `#[export(wxf)]` | typed WXF `ByteArray`    | `wxf` |
128///
129/// ```toml
130/// # Cargo.toml
131/// wolfram-export = { version = "0.6", features = ["wstp", "wxf"] }  # native is on by default
132/// ```
133///
134/// # `#[export]` — native mode (default)
135///
136/// Parameters and the return type must implement `FromArg`/`IntoArg`:
137/// `bool`, `i64`, `f64`, `String`, `NumericArray`, and references thereof.
138/// This is the fastest mode — arguments cross the LibraryLink ABI with no
139/// intermediate encoding.
140///
141/// ```
142/// # mod scope {
143/// use wolfram_export::export;
144///
145/// #[export]
146/// fn add(a: i64, b: i64) -> i64 { a + b }
147///
148/// #[export]
149/// fn greet(name: String) -> String { format!("Hello, {name}!") }
150/// # }
151/// ```
152///
153/// # `#[export(wstp)]` — WSTP mode
154///
155/// The function receives a `Vec<Expr>` (all arguments as a list) and returns
156/// an `Expr`, or takes a `&mut Link` for low-level control over the wire.
157/// Use this mode when the function's arguments or return value don't fit a
158/// fixed native/WXF shape — e.g. variadic arguments, or streaming a result
159/// incrementally.
160///
161/// ```
162/// # mod scope {
163/// use wolfram_export::export;
164/// use wolfram_expr::{expr, Expr, ExprKind};
165///
166/// // High-level: Vec<Expr> in, Expr out.
167/// #[export(wstp)]
168/// fn reverse(args: Vec<Expr>) -> Expr {
169///     let list = args.into_iter().next().expect("expected 1 arg");
170///     if let ExprKind::Normal(n) = list.kind() {
171///         let head = n.head().clone();
172///         expr!(head[..n.elements().iter().rev().cloned()])
173///     } else {
174///         list
175///     }
176/// }
177/// # }
178/// ```
179///
180/// # `#[export(wxf)]` — typed WXF mode
181///
182/// The generated wrapper reads a WXF-encoded `ByteArray` MArgument,
183/// deserializes all arguments via `FromWXF`, calls your function, and
184/// serializes the return value via `ToWXF` back into a `ByteArray`. Panics
185/// are caught and returned as structured `Failure["RustPanic", …]`
186/// expressions. Use this mode for structured arguments/return values
187/// (structs, enums, `Option`/`Result`) without hand-writing WSTP plumbing.
188///
189/// ```
190/// # mod scope {
191/// use wolfram_export::export;
192/// use wolfram_expr::{expr, Expr};
193/// use wolfram_serialize::{ToWXF, FromWXF};
194///
195/// // Primitives and Vec<T> work out of the box.
196/// #[export(wxf)]
197/// fn scale(values: Vec<f64>, factor: f64) -> Vec<f64> {
198///     values.into_iter().map(|v| v * factor).collect()
199/// }
200///
201/// // `Expr` implements `ToWXF`/`FromWXF` directly (in `wolfram-expr`), so a
202/// // function can take and return untyped Wolfram Language expressions —
203/// // useful when the shape isn't known ahead of time, or a derive isn't worth
204/// // it. Build the result with the `expr!` macro.
205/// #[export(wxf)]
206/// fn add_hold(e: Expr) -> Expr {
207///     expr!(System::Hold[e])
208/// }
209///
210/// // Structs need #[derive(ToWXF, FromWXF)].
211/// #[derive(ToWXF, FromWXF)]
212/// struct Point { x: f64, y: f64 }
213///
214/// #[export(wxf)]
215/// fn midpoint(a: Point, b: Point) -> Point {
216///     Point { x: (a.x + b.x) / 2.0, y: (a.y + b.y) / 2.0 }
217/// }
218///
219/// // Option<T> and Result<T,E> are supported too.
220/// #[export(wxf)]
221/// fn safe_div(a: f64, b: f64) -> Option<f64> {
222///     if b == 0.0 { None } else { Some(a / b) }
223/// }
224/// # }
225/// ```
226///
227/// On the Wolfram side a struct maps to an `Association`:
228///
229/// ```wolfram
230/// midpoint[<|"x" -> 0.0, "y" -> 0.0|>, <|"x" -> 2.0, "y" -> 4.0|>]
231/// (* Returns <|"x" -> 1.0, "y" -> 2.0|> *)
232/// ```
233#[proc_macro_attribute]
234pub fn export(attrs: TokenStream, item: TokenStream) -> TokenStream {
235    let attrs: syn::AttributeArgs = syn::parse_macro_input!(attrs);
236    let mode = self::export::detect_mode_from_args(&attrs);
237    let attrs = self::export::strip_wstp_arg(attrs);
238    match self::export::export(mode, attrs, item) {
239        Ok(tokens) => tokens.into(),
240        Err(err) => err.into_compile_error().into(),
241    }
242}