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