Skip to main content

wolfram_export_core/
lib.rs

1//! Shared inventory + manifest plumbing for the `wolfram-export-*` runtime
2//! crates.
3//!
4//! Hosts the [`ExportEntry`] enum (the unified inventory entry type used by
5//! all four modes — Native, Margs, Wstp, Wxf), the `inventory::collect!` declaration,
6//! and the [`exported_library_functions_association`] builder that produces
7//! the WL `Association[name -> LibraryFunctionLoad[...], ...]` Expr used by
8//! both the WSTP-mode `generate_loader!` runtime path and the WXF-mode
9//! build-time manifest path.
10//!
11//! The two transports share this one Expr-producing function — only the wire
12//! format at the boundary differs.
13
14#![warn(missing_docs)]
15
16/// The `inventory` crate, re-exported so the `wolfram-export-*` runtime crates
17/// and the macros they drive can register entries through a single shared
18/// dependency.
19#[cfg(feature = "automate-function-loading-boilerplate")]
20pub use inventory;
21
22use wolfram_expr::{expr, Association, Expr, ExprKind, RuleEntry, Symbol};
23use wolfram_serialize::{FromWXF, ToWXF};
24
25/// Serializable description of one exported function, embedded in every dylib
26/// via [`__wolfram_manifest__`]. Defined here so the CLI can share the
27/// type and deserialize directly with [`fn@wolfram_serialize::from_wxf`].
28///
29/// `params`/`ret` carry the real argument/return type `Expr`s for `Native`
30/// functions (they are unused for `Wstp`/`Wxf`, whose wire shape is fixed).
31/// Storing real `Expr`s — not stringified ones — keeps compound type specs like
32/// `List[LibraryDataType["NumericArray", "Integer8"], "Constant"]` intact.
33#[derive(ToWXF, FromWXF, Debug, Clone)]
34pub struct FunctionEntry {
35    /// Exported function name (the key used in the generated WL loader).
36    pub name: String,
37    /// Transport mode as a string: `"Native"`, `"Margs"`, `"Wstp"`, or `"Wxf"`.
38    pub kind: String,
39    /// Parameter type specs as `Expr`s (native mode only; empty otherwise).
40    pub params: Vec<Expr>,
41    /// Return type spec as an `Expr` (native mode only; a placeholder otherwise).
42    pub ret: Expr,
43}
44
45//==============================================================================
46// Loader-expression builder
47//
48// `library_functions_loader` is the single public entry point: given the built
49// libraries (each a path `Expr` + its exported functions), it produces the
50// `With[{callers…, libN = …}, <|key -> Caller[LibraryFunctionLoad[...]]|>]`
51// loader written to `Functions.wl`. Used by BOTH the `cargo wl build` CLI and
52// the runtime `exported_library_functions_association`. The private helpers
53// below have no `inventory` dependency, so they live outside the
54// `automate-function-loading-boilerplate` feature gate.
55//==============================================================================
56
57/// Which export transport a function uses. Mirrors the three `#[export]` modes
58/// and the `kind` string stored in [`FunctionEntry`].
59#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60enum ExportKind {
61    Native,
62    /// Raw MArgument export (`#[export(margs)]`). Argument/return types are
63    /// opaque to the macro, so this can never become a working
64    /// `LibraryFunctionLoad` call — [`library_function_load`] maps it
65    /// straight to `Missing["NotAvailable"]` instead.
66    Margs,
67    Wstp,
68    Wxf,
69}
70
71impl ExportKind {
72    /// Parse the `kind` string stored in a [`FunctionEntry`]; `None` if unknown.
73    fn from_kind_str(s: &str) -> Option<ExportKind> {
74        match s {
75            "Native" => Some(ExportKind::Native),
76            "Margs" => Some(ExportKind::Margs),
77            "Wstp" => Some(ExportKind::Wstp),
78            "Wxf" => Some(ExportKind::Wxf),
79            _ => None,
80        }
81    }
82}
83
84/// The `Caller` wrapper symbol name for one export kind. This is both the head
85/// of each rule's value (`NativeCaller[...]`, ...) and the name bound by
86/// [`caller_binding`] — used to detect which callers a given association
87/// actually references.
88fn caller_name(kind: ExportKind) -> &'static str {
89    match kind {
90        ExportKind::Native => "NativeCaller",
91        ExportKind::Wstp => "WSTPCaller",
92        ExportKind::Wxf => "WXFCaller",
93        // Margs entries are never wrapped in a caller (see
94        // `library_function_load`) and `with_callers` never iterates this
95        // variant, so this is never reached.
96        ExportKind::Margs => unreachable!("margs mode never needs a caller binding"),
97    }
98}
99
100/// The `Set[Caller, ...]` prelude binding for one export kind. Emitted (only
101/// when referenced — see [`with_callers`]) as part of the `With[{...}, <|...|>]`
102/// loader so the per-function `Caller[LibraryFunctionLoad[...]]` rules from
103/// [`library_function_rule`] resolve.
104///
105/// - `NativeCaller = Identity` — the loaded function is used directly.
106/// - `WSTPCaller` resets `$Context`/`$ContextPath` around the call for
107///   predictable symbol resolution across the link.
108/// - `WXFCaller` serializes the args and deserializes the result so the WXF
109///   `{ByteArray} -> ByteArray` function presents a normal-expression interface.
110fn caller_binding(kind: ExportKind) -> Expr {
111    match kind {
112        ExportKind::Native => expr!(::Set[::NativeCaller, ::Identity]),
113        ExportKind::Wstp => expr!(::Set[
114            ::WSTPCaller,
115            ::Function[::With[
116                ::List[::Set[::f, ::Slot[1]]],
117                ::Function[::Block[
118                    ::List[
119                        ::Set[::$Context, "RustLinkWSTPPrivateContext`"],
120                        ::Set[::$ContextPath, ::List[]]
121                    ],
122                    ::f[::SlotSequence[1]]
123                ]]
124            ]]
125        ]),
126        ExportKind::Wxf => expr!(::Set[
127            ::WXFCaller,
128            ::Function[::Composition[
129                ::BinaryDeserialize,
130                ::Slot[1],
131                ::BinarySerialize,
132                ::List
133            ]]
134        ]),
135        ExportKind::Margs => unreachable!("margs mode never needs a caller binding"),
136    }
137}
138
139/// Build `Caller[LibraryFunctionLoad[lib, name, <args>, <ret>]]` for one
140/// exported function — every kind always produces a real
141/// `LibraryFunctionLoad` call. [`ExportKind::Margs`] carries a real
142/// `native_sig` unconditionally too: the macro always materializes one —
143/// either the user's `#[export(margs, args = .., ret = ..)]` annotation, or
144/// (with a compile-time warning) a default of the same fixed
145/// `LinkObject`/`LinkObject` placeholder `ExportKind::Wstp` uses. So `Margs`
146/// is handled identically to `Native` here — same wire ABI, same
147/// `NativeCaller = Identity` wrapper.
148///
149/// `lib` is any `Expr` that evaluates to the dylib path — a string literal at
150/// runtime, or a `libN` symbol bound in the generated `Functions.wl` prelude.
151/// `native_sig` supplies the real argument/return type `Expr`s for
152/// [`ExportKind::Native`] and [`ExportKind::Margs`]; for `Wstp`/`Wxf` the
153/// wire shape is fixed and it is ignored.
154fn library_function_load(
155    kind: ExportKind,
156    name: &str,
157    lib: Expr,
158    native_sig: Option<(Vec<Expr>, Expr)>,
159) -> Expr {
160    // `(arg-type spec, return-type spec)` — the 3rd and 4th positional args to
161    // LibraryFunctionLoad. Native/Margs carry the real signature; Wstp passes
162    // the bare `LinkObject` marker; Wxf is always
163    // `{{ByteArray,"Constant"}} -> ByteArray` ("Constant" is the no-mutate
164    // promise that lets the kernel skip a deep copy of the serialized input).
165    let (args, ret): (Expr, Expr) = match kind {
166        ExportKind::Native | ExportKind::Margs => {
167            let (args, ret) =
168                native_sig.unwrap_or_else(|| (Vec::new(), Expr::string("")));
169            (Expr::from(args), ret)
170        },
171        ExportKind::Wstp => (expr!(::LinkObject), expr!(::LinkObject)),
172        ExportKind::Wxf => (
173            expr!(::List[::List[::ByteArray, "Constant"]]),
174            expr!(::ByteArray),
175        ),
176    };
177    let load = expr!(::LibraryFunctionLoad[lib, name, args, ret]);
178    match kind {
179        ExportKind::Native | ExportKind::Margs => expr!(::NativeCaller[load]),
180        ExportKind::Wstp => expr!(::WSTPCaller[load]),
181        ExportKind::Wxf => expr!(::WXFCaller[load]),
182    }
183}
184
185/// Wrap a finished association in `With[bindings, assoc]`, emitting ONLY the
186/// caller prelude bindings (`NativeCaller`/`WSTPCaller`/`WXFCaller`) actually
187/// referenced by the rules, followed by `extra` (e.g. the CLI's
188/// `libN = FileNameJoin[...]` path bindings). A loader with no WXF functions, for
189/// instance, omits the `WXFCaller` binding entirely.
190fn with_callers(extra: Vec<Expr>, assoc: Association) -> Expr {
191    let used = |kind: ExportKind| {
192        let name = caller_name(kind);
193        assoc.iter().any(|entry| {
194            // Each rule value is `Caller[LibraryFunctionLoad[...]]`; the head
195            // symbol identifies which caller it needs.
196            matches!(
197                entry.value.normal_head().as_ref().map(Expr::kind),
198                Some(ExprKind::Symbol(s)) if s.as_str() == name
199            )
200        })
201    };
202
203    let mut bindings: Vec<Expr> = [ExportKind::Native, ExportKind::Wstp, ExportKind::Wxf]
204        .iter()
205        .copied()
206        .filter(|&kind| used(kind))
207        .map(caller_binding)
208        .collect();
209    bindings.extend(extra);
210    expr!(::With[bindings, assoc])
211}
212
213/// Compute the association key for an exported function: `"namespace::name"`
214/// when a namespace is supplied, otherwise the bare `name`.
215fn export_key(namespace: Option<&str>, name: &str) -> String {
216    match namespace {
217        Some(ns) => format!("{ns}::{name}"),
218        None => name.to_owned(),
219    }
220}
221
222/// One built library to include in a generated paclet loader: where to find the
223/// library at load time, plus the functions it exports.
224pub struct LibraryArtifact {
225    /// A Wolfram Language expression that evaluates to the library file's path
226    /// when `Functions.wl` is loaded. The CLI passes something like
227    /// `FileNameJoin[{DirectoryName[$InputFileName], "abc123.dylib"}]`; the
228    /// runtime passes a plain absolute-path string.
229    pub path: Expr,
230    /// When `Some(ns)`, every function key is namespaced as `"ns::name"` so
231    /// functions from different libraries cannot collide.
232    pub namespace: Option<String>,
233    /// The functions this library exports, as decoded from its embedded
234    /// manifest (see [`FunctionEntry`]).
235    pub functions: Vec<FunctionEntry>,
236}
237
238/// Build the loader association written to `Functions.wl`, the single entry
239/// point shared by the `cargo wl build` CLI and the runtime WSTP loader.
240///
241/// Produces `With[{<caller prelude…>, lib1 = path1, …}, <|key -> Caller[LibraryFunctionLoad[…]], …|>]`,
242/// where each library's `path` is bound to a `libN` symbol that its functions
243/// reference. Only the caller-prelude bindings (`NativeCaller`/`WSTPCaller`/
244/// `WXFCaller`) actually used are emitted, and libraries with no functions are
245/// skipped.
246pub fn library_functions_loader(libraries: &[LibraryArtifact]) -> Expr {
247    let mut bindings: Vec<Expr> = Vec::new();
248    let mut rules: Vec<RuleEntry> = Vec::new();
249
250    let mut n = 0;
251    for library in libraries {
252        if library.functions.is_empty() {
253            continue;
254        }
255        n += 1;
256        let libvar = Symbol::new(&format!("lib{n}"));
257        bindings.push(expr!(::Set[(Expr::from(libvar.clone())), (library.path.clone())]));
258
259        for entry in &library.functions {
260            let Some(kind) = ExportKind::from_kind_str(&entry.kind) else {
261                continue;
262            };
263            let native_sig = match kind {
264                ExportKind::Native | ExportKind::Margs => {
265                    Some((entry.params.clone(), entry.ret.clone()))
266                },
267                _ => None,
268            };
269            let key = export_key(library.namespace.as_deref(), &entry.name);
270            rules.push(RuleEntry::rule(
271                Expr::from(key.as_str()),
272                library_function_load(
273                    kind,
274                    &entry.name,
275                    Expr::from(libvar.clone()),
276                    native_sig,
277                ),
278            ));
279        }
280    }
281
282    with_callers(bindings, rules.into_iter().collect())
283}
284
285/// Inventory entry for one `#[export]`-marked function.
286///
287/// Replaces the legacy `LibraryLinkFunction` enum from `wolfram-library-link`.
288/// All three export-mode runtimes (`wolfram-export-native`, `wolfram-export-wstp`,
289/// `wolfram-export-wxf`) submit entries of this single shared type to one
290/// global inventory; [`exported_library_functions_association`] iterates that
291/// inventory regardless of mode.
292pub enum ExportEntry {
293    /// Native MArgument-based export.
294    Native {
295        /// Exported symbol name (matches the `#[no_mangle] extern "C"` symbol).
296        name: &'static str,
297        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s.
298        ///
299        /// See the implementation note on `LibraryLinkFunction::Native::signature`
300        /// for why this is a `fn` pointer rather than a `Box<dyn ...>`.
301        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
302    },
303    /// Raw MArgument-based export (`#[export(margs)]`). Argument/return types
304    /// are opaque to the macro by default (the user marshals
305    /// `&[MArgument]`/`MArgument` by hand) — unlike `Native`, whose signature
306    /// is inferred from `FromArg`/`IntoArg` impls, there's nothing to infer
307    /// here. `signature` is never absent, though: the macro always
308    /// materializes one at expansion time — the user's
309    /// `#[export(margs, args = ..., ret = ...)]` annotation if given, or
310    /// (with a compile-time warning) a default of the same fixed
311    /// `LinkObject`/`LinkObject` placeholder `Wstp` uses.
312    Margs {
313        /// Exported symbol name.
314        name: &'static str,
315        /// (arg types, return type) — user-declared, or the macro's own
316        /// `LinkObject`/`LinkObject` default.
317        signature: fn() -> (Vec<Expr>, Expr),
318    },
319    /// WSTP `LinkObject`-based export.
320    Wstp {
321        /// Exported symbol name.
322        name: &'static str,
323    },
324    /// Typed-args WXF-based export (NEW). Wire shape is `{ByteArray} -> ByteArray`
325    /// at the LibraryLink level; the byte arrays carry WXF-encoded payloads of
326    /// the user-declared Rust types.
327    Wxf {
328        /// Exported symbol name.
329        name: &'static str,
330        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s
331        /// — used for the manifest's typed signature display, not for the WL-side
332        /// `LibraryFunctionLoad` call (which is always `{ByteArray} -> ByteArray`).
333        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
334    },
335}
336
337#[cfg(feature = "automate-function-loading-boilerplate")]
338inventory::collect!(ExportEntry);
339
340//==============================================================================
341// __wolfram_manifest__: build-time-extractable manifest symbol
342//==============================================================================
343
344/// C-ABI symbol called via `dlopen` at build time to extract the library's
345/// exported-function manifest without running a WSTP loop.
346///
347/// Returns a pointer to a leaked buffer whose first 8 bytes are the WXF payload
348/// length as a little-endian `u64`, followed immediately by the WXF-serialized
349/// `Vec<FunctionEntry>`. Deserialize with:
350/// ```ignore
351/// let len = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
352/// wolfram_serialize::deserialize::<Vec<FunctionEntry>>(&buf[8..8+len], None)
353/// ```
354#[cfg(feature = "automate-function-loading-boilerplate")]
355#[no_mangle]
356pub extern "C" fn __wolfram_manifest__() -> *const u8 {
357    let entries: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
358        .filter_map(ExportEntry::to_function_entry)
359        .collect();
360
361    let wxf = wolfram_serialize::to_wxf(&entries, None)
362        .expect("manifest WXF serialization failed");
363    // Prepend the payload length as 8 little-endian bytes so the caller needs
364    // no out-parameter — one zero-arg call, read [0..8] for the length, [8..] for WXF.
365    let mut buf = Vec::with_capacity(8 + wxf.len());
366    buf.extend_from_slice(&(wxf.len() as u64).to_le_bytes());
367    buf.extend_from_slice(&wxf);
368    Box::leak(buf.into_boxed_slice()).as_ptr()
369}
370
371/// Returns an [`Association`][Association] containing the names and `LibraryFunctionLoad`
372/// calls for every `#[export(..)]`-marked function in this library.
373///
374/// Iterates the shared inventory built up by `inventory::submit!` calls from
375/// the three export-mode runtimes. Same Association shape today's
376/// `wolfram-library-link::exported_library_functions_association` produces,
377/// plus an extra arm for the new `Wxf` mode.
378///
379/// `library` overrides automatic dylib path detection.
380///
381/// [Association]: https://reference.wolfram.com/language/ref/Association.html
382#[cfg(feature = "automate-function-loading-boilerplate")]
383pub fn exported_library_functions_association(
384    library: Option<std::path::PathBuf>,
385) -> Expr {
386    let library: std::path::PathBuf = library.unwrap_or_else(|| {
387        process_path::get_dylib_path()
388            .expect("unable to automatically determine Rust LibraryLink dynamic library file path. Suggestion: pass the library name or path to exported_library_functions_association(..)")
389    });
390    let library = library
391        .to_str()
392        .expect("unable to convert library file path to str");
393
394    // A single dylib's inventory has no cross-library clashes, so no
395    // namespacing; the runtime loads from the resolved absolute path.
396    let functions: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
397        .filter_map(ExportEntry::to_function_entry)
398        .collect();
399    library_functions_loader(&[LibraryArtifact {
400        path: Expr::string(library),
401        namespace: None,
402        functions,
403    }])
404}
405
406#[cfg(feature = "automate-function-loading-boilerplate")]
407impl ExportEntry {
408    /// Convert an inventory entry into a serializable [`FunctionEntry`].
409    ///
410    /// Returns `None` for a `Native` function whose signature cannot be
411    /// resolved (e.g. an unsupported argument type) — it could not be loaded,
412    /// so it is omitted from both the manifest and the loader. `Wstp`/`Wxf`
413    /// have a fixed wire shape and carry empty `params`/`ret` placeholders.
414    /// `Margs`'s `signature` never fails to resolve — the macro always
415    /// materializes one, whether from the user's annotation or its own
416    /// `LinkObject`/`LinkObject` default — so it's never omitted either.
417    fn to_function_entry(&self) -> Option<FunctionEntry> {
418        Some(match self {
419            ExportEntry::Native { name, signature } => {
420                let (params, ret) = signature().ok()?;
421                FunctionEntry {
422                    name: (*name).to_owned(),
423                    kind: "Native".to_owned(),
424                    params,
425                    ret,
426                }
427            },
428            ExportEntry::Margs { name, signature } => {
429                let (params, ret) = signature();
430                FunctionEntry {
431                    name: (*name).to_owned(),
432                    kind: "Margs".to_owned(),
433                    params,
434                    ret,
435                }
436            },
437            ExportEntry::Wstp { name } => FunctionEntry {
438                name: (*name).to_owned(),
439                kind: "Wstp".to_owned(),
440                params: vec![],
441                ret: Expr::string(""),
442            },
443            ExportEntry::Wxf { name, .. } => FunctionEntry {
444                name: (*name).to_owned(),
445                kind: "Wxf".to_owned(),
446                params: vec![],
447                ret: Expr::string(""),
448            },
449        })
450    }
451}