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(kind, &entry.name, Expr::from(libvar.clone()), native_sig),
253 ));
254 }
255 }
256
257 with_callers(bindings, rules.into_iter().collect())
258}
259
260/// Inventory entry for one `#[export]`-marked function.
261///
262/// Replaces the legacy `LibraryLinkFunction` enum from `wolfram-library-link`.
263/// All three export-mode runtimes (`wolfram-export-native`, `wolfram-export-wstp`,
264/// `wolfram-export-wxf`) submit entries of this single shared type to one
265/// global inventory; [`exported_library_functions_association`] iterates that
266/// inventory regardless of mode.
267pub enum ExportEntry {
268 /// Native MArgument-based export.
269 Native {
270 /// Exported symbol name (matches the `#[no_mangle] extern "C"` symbol).
271 name: &'static str,
272 /// Closure returning (arg types, return type) as Wolfram Language `Expr`s.
273 ///
274 /// See the implementation note on `LibraryLinkFunction::Native::signature`
275 /// for why this is a `fn` pointer rather than a `Box<dyn ...>`.
276 signature: fn() -> Result<(Vec<Expr>, Expr), String>,
277 },
278 /// WSTP `LinkObject`-based export.
279 Wstp {
280 /// Exported symbol name.
281 name: &'static str,
282 },
283 /// Typed-args WXF-based export (NEW). Wire shape is `{ByteArray} -> ByteArray`
284 /// at the LibraryLink level; the byte arrays carry WXF-encoded payloads of
285 /// the user-declared Rust types.
286 Wxf {
287 /// Exported symbol name.
288 name: &'static str,
289 /// Closure returning (arg types, return type) as Wolfram Language `Expr`s
290 /// — used for the manifest's typed signature display, not for the WL-side
291 /// `LibraryFunctionLoad` call (which is always `{ByteArray} -> ByteArray`).
292 signature: fn() -> Result<(Vec<Expr>, Expr), String>,
293 },
294}
295
296#[cfg(feature = "automate-function-loading-boilerplate")]
297inventory::collect!(ExportEntry);
298
299//==============================================================================
300// __wolfram_manifest__: build-time-extractable manifest symbol
301//==============================================================================
302
303/// C-ABI symbol called via `dlopen` at build time to extract the library's
304/// exported-function manifest without running a WSTP loop.
305///
306/// Returns a pointer to a leaked buffer whose first 8 bytes are the WXF payload
307/// length as a little-endian `u64`, followed immediately by the WXF-serialized
308/// `Vec<FunctionEntry>`. Deserialize with:
309/// ```ignore
310/// let len = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
311/// wolfram_serialize::deserialize::<Vec<FunctionEntry>>(&buf[8..8+len], None)
312/// ```
313#[cfg(feature = "automate-function-loading-boilerplate")]
314#[no_mangle]
315pub extern "C" fn __wolfram_manifest__() -> *const u8 {
316 let entries: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
317 .filter_map(ExportEntry::to_function_entry)
318 .collect();
319
320 let wxf =
321 wolfram_serialize::to_wxf(&entries, None).expect("manifest WXF serialization failed");
322 // Prepend the payload length as 8 little-endian bytes so the caller needs
323 // no out-parameter — one zero-arg call, read [0..8] for the length, [8..] for WXF.
324 let mut buf = Vec::with_capacity(8 + wxf.len());
325 buf.extend_from_slice(&(wxf.len() as u64).to_le_bytes());
326 buf.extend_from_slice(&wxf);
327 Box::leak(buf.into_boxed_slice()).as_ptr()
328}
329
330/// Returns an [`Association`][Association] containing the names and `LibraryFunctionLoad`
331/// calls for every `#[export(..)]`-marked function in this library.
332///
333/// Iterates the shared inventory built up by `inventory::submit!` calls from
334/// the three export-mode runtimes. Same Association shape today's
335/// `wolfram-library-link::exported_library_functions_association` produces,
336/// plus an extra arm for the new `Wxf` mode.
337///
338/// `library` overrides automatic dylib path detection.
339///
340/// [Association]: https://reference.wolfram.com/language/ref/Association.html
341#[cfg(feature = "automate-function-loading-boilerplate")]
342pub fn exported_library_functions_association(
343 library: Option<std::path::PathBuf>,
344) -> Expr {
345 let library: std::path::PathBuf = library.unwrap_or_else(|| {
346 process_path::get_dylib_path()
347 .expect("unable to automatically determine Rust LibraryLink dynamic library file path. Suggestion: pass the library name or path to exported_library_functions_association(..)")
348 });
349 let library = library
350 .to_str()
351 .expect("unable to convert library file path to str");
352
353 // A single dylib's inventory has no cross-library clashes, so no
354 // namespacing; the runtime loads from the resolved absolute path.
355 let functions: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
356 .filter_map(ExportEntry::to_function_entry)
357 .collect();
358 library_functions_loader(&[LibraryArtifact {
359 path: Expr::string(library),
360 namespace: None,
361 functions,
362 }])
363}
364
365#[cfg(feature = "automate-function-loading-boilerplate")]
366impl ExportEntry {
367 /// Convert an inventory entry into a serializable [`FunctionEntry`].
368 ///
369 /// Returns `None` for a `Native` function whose signature cannot be
370 /// resolved (e.g. an unsupported argument type) — it could not be loaded,
371 /// so it is omitted from both the manifest and the loader. `Wstp`/`Wxf`
372 /// have a fixed wire shape and carry empty `params`/`ret` placeholders.
373 fn to_function_entry(&self) -> Option<FunctionEntry> {
374 Some(match self {
375 ExportEntry::Native { name, signature } => {
376 let (params, ret) = signature().ok()?;
377 FunctionEntry {
378 name: (*name).to_owned(),
379 kind: "Native".to_owned(),
380 params,
381 ret,
382 }
383 },
384 ExportEntry::Wstp { name } => FunctionEntry {
385 name: (*name).to_owned(),
386 kind: "Wstp".to_owned(),
387 params: vec![],
388 ret: Expr::string(""),
389 },
390 ExportEntry::Wxf { name, .. } => FunctionEntry {
391 name: (*name).to_owned(),
392 kind: "Wxf".to_owned(),
393 params: vec![],
394 ret: Expr::string(""),
395 },
396 })
397 }
398}