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 three modes — Native, 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"`, `"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    Wstp,
63    Wxf,
64}
65
66impl ExportKind {
67    /// Parse the `kind` string stored in a [`FunctionEntry`]; `None` if unknown.
68    fn from_kind_str(s: &str) -> Option<ExportKind> {
69        match s {
70            "Native" => Some(ExportKind::Native),
71            "Wstp" => Some(ExportKind::Wstp),
72            "Wxf" => Some(ExportKind::Wxf),
73            _ => None,
74        }
75    }
76}
77
78/// The `Caller` wrapper symbol name for one export kind. This is both the head
79/// of each rule's value (`NativeCaller[...]`, ...) and the name bound by
80/// [`caller_binding`] — used to detect which callers a given association
81/// actually references.
82fn caller_name(kind: ExportKind) -> &'static str {
83    match kind {
84        ExportKind::Native => "NativeCaller",
85        ExportKind::Wstp => "WSTPCaller",
86        ExportKind::Wxf => "WXFCaller",
87    }
88}
89
90/// The `Set[Caller, ...]` prelude binding for one export kind. Emitted (only
91/// when referenced — see [`with_callers`]) as part of the `With[{...}, <|...|>]`
92/// loader so the per-function `Caller[LibraryFunctionLoad[...]]` rules from
93/// [`library_function_rule`] resolve.
94///
95/// - `NativeCaller = Identity` — the loaded function is used directly.
96/// - `WSTPCaller` resets `$Context`/`$ContextPath` around the call for
97///   predictable symbol resolution across the link.
98/// - `WXFCaller` serializes the args and deserializes the result so the WXF
99///   `{ByteArray} -> ByteArray` function presents a normal-expression interface.
100fn caller_binding(kind: ExportKind) -> Expr {
101    match kind {
102        ExportKind::Native => expr!(::Set[::NativeCaller, ::Identity]),
103        ExportKind::Wstp => expr!(::Set[
104            ::WSTPCaller,
105            ::Function[::With[
106                ::List[::Set[::f, ::Slot[1]]],
107                ::Function[::Block[
108                    ::List[
109                        ::Set[::$Context, "RustLinkWSTPPrivateContext`"],
110                        ::Set[::$ContextPath, ::List[]]
111                    ],
112                    ::f[::SlotSequence[1]]
113                ]]
114            ]]
115        ]),
116        ExportKind::Wxf => expr!(::Set[
117            ::WXFCaller,
118            ::Function[::Composition[
119                ::BinaryDeserialize,
120                ::Slot[1],
121                ::BinarySerialize,
122                ::List
123            ]]
124        ]),
125    }
126}
127
128/// Build `Caller[LibraryFunctionLoad[lib, name, <args>, <ret>]]` for one
129/// exported function.
130///
131/// `lib` is any `Expr` that evaluates to the dylib path — a string literal at
132/// runtime, or a `libN` symbol bound in the generated `Functions.wl` prelude.
133/// `native_sig` supplies the real argument/return type `Expr`s for
134/// [`ExportKind::Native`]; for `Wstp`/`Wxf` the wire shape is fixed and it is
135/// ignored.
136fn library_function_load(
137    kind: ExportKind,
138    name: &str,
139    lib: Expr,
140    native_sig: Option<(Vec<Expr>, Expr)>,
141) -> Expr {
142    // `(arg-type spec, return-type spec)` — the 3rd and 4th positional args to
143    // LibraryFunctionLoad. Native carries the real signature; Wstp passes the
144    // bare `LinkObject` marker; Wxf is always `{{ByteArray,"Constant"}} -> ByteArray`
145    // ("Constant" is the no-mutate promise that lets the kernel skip a deep copy
146    // of the serialized input).
147    let (args, ret): (Expr, Expr) = match kind {
148        ExportKind::Native => {
149            let (args, ret) =
150                native_sig.unwrap_or_else(|| (Vec::new(), Expr::string("")));
151            (Expr::from(args), ret)
152        },
153        ExportKind::Wstp => (expr!(::LinkObject), expr!(::LinkObject)),
154        ExportKind::Wxf => (
155            expr!(::List[::List[::ByteArray, "Constant"]]),
156            expr!(::ByteArray),
157        ),
158    };
159    let load = expr!(::LibraryFunctionLoad[lib, name, args, ret]);
160    match kind {
161        ExportKind::Native => expr!(::NativeCaller[load]),
162        ExportKind::Wstp => expr!(::WSTPCaller[load]),
163        ExportKind::Wxf => expr!(::WXFCaller[load]),
164    }
165}
166
167/// Wrap a finished association in `With[bindings, assoc]`, emitting ONLY the
168/// caller prelude bindings (`NativeCaller`/`WSTPCaller`/`WXFCaller`) actually
169/// referenced by the rules, followed by `extra` (e.g. the CLI's
170/// `libN = FileNameJoin[...]` path bindings). A loader with no WXF functions, for
171/// instance, omits the `WXFCaller` binding entirely.
172fn with_callers(extra: Vec<Expr>, assoc: Association) -> Expr {
173    let used = |kind: ExportKind| {
174        let name = caller_name(kind);
175        assoc.iter().any(|entry| {
176            // Each rule value is `Caller[LibraryFunctionLoad[...]]`; the head
177            // symbol identifies which caller it needs.
178            matches!(
179                entry.value.normal_head().as_ref().map(Expr::kind),
180                Some(ExprKind::Symbol(s)) if s.as_str() == name
181            )
182        })
183    };
184
185    let mut bindings: Vec<Expr> = [ExportKind::Native, ExportKind::Wstp, ExportKind::Wxf]
186        .iter()
187        .copied()
188        .filter(|&kind| used(kind))
189        .map(caller_binding)
190        .collect();
191    bindings.extend(extra);
192    expr!(::With[bindings, assoc])
193}
194
195/// Compute the association key for an exported function: `"namespace::name"`
196/// when a namespace is supplied, otherwise the bare `name`.
197fn export_key(namespace: Option<&str>, name: &str) -> String {
198    match namespace {
199        Some(ns) => format!("{ns}::{name}"),
200        None => name.to_owned(),
201    }
202}
203
204/// One built library to include in a generated paclet loader: where to find the
205/// library at load time, plus the functions it exports.
206pub struct LibraryArtifact {
207    /// A Wolfram Language expression that evaluates to the library file's path
208    /// when `Functions.wl` is loaded. The CLI passes something like
209    /// `FileNameJoin[{DirectoryName[$InputFileName], "abc123.dylib"}]`; the
210    /// runtime passes a plain absolute-path string.
211    pub path: Expr,
212    /// When `Some(ns)`, every function key is namespaced as `"ns::name"` so
213    /// functions from different libraries cannot collide.
214    pub namespace: Option<String>,
215    /// The functions this library exports, as decoded from its embedded
216    /// manifest (see [`FunctionEntry`]).
217    pub functions: Vec<FunctionEntry>,
218}
219
220/// Build the loader association written to `Functions.wl`, the single entry
221/// point shared by the `cargo wl build` CLI and the runtime WSTP loader.
222///
223/// Produces `With[{<caller prelude…>, lib1 = path1, …}, <|key -> Caller[LibraryFunctionLoad[…]], …|>]`,
224/// where each library's `path` is bound to a `libN` symbol that its functions
225/// reference. Only the caller-prelude bindings (`NativeCaller`/`WSTPCaller`/
226/// `WXFCaller`) actually used are emitted, and libraries with no functions are
227/// skipped.
228pub fn library_functions_loader(libraries: &[LibraryArtifact]) -> Expr {
229    let mut bindings: Vec<Expr> = Vec::new();
230    let mut rules: Vec<RuleEntry> = Vec::new();
231
232    let mut n = 0;
233    for library in libraries {
234        if library.functions.is_empty() {
235            continue;
236        }
237        n += 1;
238        let libvar = Symbol::new(&format!("lib{n}"));
239        bindings.push(expr!(::Set[(Expr::from(libvar.clone())), (library.path.clone())]));
240
241        for entry in &library.functions {
242            let Some(kind) = ExportKind::from_kind_str(&entry.kind) else {
243                continue;
244            };
245            let native_sig = match kind {
246                ExportKind::Native => Some((entry.params.clone(), entry.ret.clone())),
247                _ => None,
248            };
249            let key = export_key(library.namespace.as_deref(), &entry.name);
250            rules.push(RuleEntry::rule(
251                Expr::from(key.as_str()),
252                library_function_load(
253                    kind,
254                    &entry.name,
255                    Expr::from(libvar.clone()),
256                    native_sig,
257                ),
258            ));
259        }
260    }
261
262    with_callers(bindings, rules.into_iter().collect())
263}
264
265/// Inventory entry for one `#[export]`-marked function.
266///
267/// Replaces the legacy `LibraryLinkFunction` enum from `wolfram-library-link`.
268/// All three export-mode runtimes (`wolfram-export-native`, `wolfram-export-wstp`,
269/// `wolfram-export-wxf`) submit entries of this single shared type to one
270/// global inventory; [`exported_library_functions_association`] iterates that
271/// inventory regardless of mode.
272pub enum ExportEntry {
273    /// Native MArgument-based export.
274    Native {
275        /// Exported symbol name (matches the `#[no_mangle] extern "C"` symbol).
276        name: &'static str,
277        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s.
278        ///
279        /// See the implementation note on `LibraryLinkFunction::Native::signature`
280        /// for why this is a `fn` pointer rather than a `Box<dyn ...>`.
281        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
282    },
283    /// WSTP `LinkObject`-based export.
284    Wstp {
285        /// Exported symbol name.
286        name: &'static str,
287    },
288    /// Typed-args WXF-based export (NEW). Wire shape is `{ByteArray} -> ByteArray`
289    /// at the LibraryLink level; the byte arrays carry WXF-encoded payloads of
290    /// the user-declared Rust types.
291    Wxf {
292        /// Exported symbol name.
293        name: &'static str,
294        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s
295        /// — used for the manifest's typed signature display, not for the WL-side
296        /// `LibraryFunctionLoad` call (which is always `{ByteArray} -> ByteArray`).
297        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
298    },
299}
300
301#[cfg(feature = "automate-function-loading-boilerplate")]
302inventory::collect!(ExportEntry);
303
304//==============================================================================
305// __wolfram_manifest__: build-time-extractable manifest symbol
306//==============================================================================
307
308/// C-ABI symbol called via `dlopen` at build time to extract the library's
309/// exported-function manifest without running a WSTP loop.
310///
311/// Returns a pointer to a leaked buffer whose first 8 bytes are the WXF payload
312/// length as a little-endian `u64`, followed immediately by the WXF-serialized
313/// `Vec<FunctionEntry>`. Deserialize with:
314/// ```ignore
315/// let len = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
316/// wolfram_serialize::deserialize::<Vec<FunctionEntry>>(&buf[8..8+len], None)
317/// ```
318#[cfg(feature = "automate-function-loading-boilerplate")]
319#[no_mangle]
320pub extern "C" fn __wolfram_manifest__() -> *const u8 {
321    let entries: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
322        .filter_map(ExportEntry::to_function_entry)
323        .collect();
324
325    let wxf = wolfram_serialize::to_wxf(&entries, None)
326        .expect("manifest WXF serialization failed");
327    // Prepend the payload length as 8 little-endian bytes so the caller needs
328    // no out-parameter — one zero-arg call, read [0..8] for the length, [8..] for WXF.
329    let mut buf = Vec::with_capacity(8 + wxf.len());
330    buf.extend_from_slice(&(wxf.len() as u64).to_le_bytes());
331    buf.extend_from_slice(&wxf);
332    Box::leak(buf.into_boxed_slice()).as_ptr()
333}
334
335/// Returns an [`Association`][Association] containing the names and `LibraryFunctionLoad`
336/// calls for every `#[export(..)]`-marked function in this library.
337///
338/// Iterates the shared inventory built up by `inventory::submit!` calls from
339/// the three export-mode runtimes. Same Association shape today's
340/// `wolfram-library-link::exported_library_functions_association` produces,
341/// plus an extra arm for the new `Wxf` mode.
342///
343/// `library` overrides automatic dylib path detection.
344///
345/// [Association]: https://reference.wolfram.com/language/ref/Association.html
346#[cfg(feature = "automate-function-loading-boilerplate")]
347pub fn exported_library_functions_association(
348    library: Option<std::path::PathBuf>,
349) -> Expr {
350    let library: std::path::PathBuf = library.unwrap_or_else(|| {
351        process_path::get_dylib_path()
352            .expect("unable to automatically determine Rust LibraryLink dynamic library file path. Suggestion: pass the library name or path to exported_library_functions_association(..)")
353    });
354    let library = library
355        .to_str()
356        .expect("unable to convert library file path to str");
357
358    // A single dylib's inventory has no cross-library clashes, so no
359    // namespacing; the runtime loads from the resolved absolute path.
360    let functions: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
361        .filter_map(ExportEntry::to_function_entry)
362        .collect();
363    library_functions_loader(&[LibraryArtifact {
364        path: Expr::string(library),
365        namespace: None,
366        functions,
367    }])
368}
369
370#[cfg(feature = "automate-function-loading-boilerplate")]
371impl ExportEntry {
372    /// Convert an inventory entry into a serializable [`FunctionEntry`].
373    ///
374    /// Returns `None` for a `Native` function whose signature cannot be
375    /// resolved (e.g. an unsupported argument type) — it could not be loaded,
376    /// so it is omitted from both the manifest and the loader. `Wstp`/`Wxf`
377    /// have a fixed wire shape and carry empty `params`/`ret` placeholders.
378    fn to_function_entry(&self) -> Option<FunctionEntry> {
379        Some(match self {
380            ExportEntry::Native { name, signature } => {
381                let (params, ret) = signature().ok()?;
382                FunctionEntry {
383                    name: (*name).to_owned(),
384                    kind: "Native".to_owned(),
385                    params,
386                    ret,
387                }
388            },
389            ExportEntry::Wstp { name } => FunctionEntry {
390                name: (*name).to_owned(),
391                kind: "Wstp".to_owned(),
392                params: vec![],
393                ret: Expr::string(""),
394            },
395            ExportEntry::Wxf { name, .. } => FunctionEntry {
396                name: (*name).to_owned(),
397                kind: "Wxf".to_owned(),
398                params: vec![],
399                ret: Expr::string(""),
400            },
401        })
402    }
403}