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}