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}