wolfram-export-core 0.6.0

Shared inventory + manifest plumbing for the wolfram-export-* crates
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! Shared inventory + manifest plumbing for the `wolfram-export-*` runtime
//! crates.
//!
//! Hosts the [`ExportEntry`] enum (the unified inventory entry type used by
//! all four modes — Native, Margs, Wstp, Wxf), the `inventory::collect!` declaration,
//! and the [`exported_library_functions_association`] builder that produces
//! the WL `Association[name -> LibraryFunctionLoad[...], ...]` Expr used by
//! both the WSTP-mode `generate_loader!` runtime path and the WXF-mode
//! build-time manifest path.
//!
//! The two transports share this one Expr-producing function — only the wire
//! format at the boundary differs.

#![warn(missing_docs)]

/// The `inventory` crate, re-exported so the `wolfram-export-*` runtime crates
/// and the macros they drive can register entries through a single shared
/// dependency.
#[cfg(feature = "automate-function-loading-boilerplate")]
pub use inventory;

use wolfram_expr::{expr, Association, Expr, ExprKind, RuleEntry, Symbol};
use wolfram_serialize::{FromWXF, ToWXF};

/// Serializable description of one exported function, embedded in every dylib
/// via [`__wolfram_manifest__`]. Defined here so the CLI can share the
/// type and deserialize directly with [`fn@wolfram_serialize::from_wxf`].
///
/// `params`/`ret` carry the real argument/return type `Expr`s for `Native`
/// functions (they are unused for `Wstp`/`Wxf`, whose wire shape is fixed).
/// Storing real `Expr`s — not stringified ones — keeps compound type specs like
/// `List[LibraryDataType["NumericArray", "Integer8"], "Constant"]` intact.
#[derive(ToWXF, FromWXF, Debug, Clone)]
pub struct FunctionEntry {
    /// Exported function name (the key used in the generated WL loader).
    pub name: String,
    /// Transport mode as a string: `"Native"`, `"Margs"`, `"Wstp"`, or `"Wxf"`.
    pub kind: String,
    /// Parameter type specs as `Expr`s (native mode only; empty otherwise).
    pub params: Vec<Expr>,
    /// Return type spec as an `Expr` (native mode only; a placeholder otherwise).
    pub ret: Expr,
}

//==============================================================================
// Loader-expression builder
//
// `library_functions_loader` is the single public entry point: given the built
// libraries (each a path `Expr` + its exported functions), it produces the
// `With[{callers…, libN = …}, <|key -> Caller[LibraryFunctionLoad[...]]|>]`
// loader written to `Functions.wl`. Used by BOTH the `cargo wl build` CLI and
// the runtime `exported_library_functions_association`. The private helpers
// below have no `inventory` dependency, so they live outside the
// `automate-function-loading-boilerplate` feature gate.
//==============================================================================

/// Which export transport a function uses. Mirrors the three `#[export]` modes
/// and the `kind` string stored in [`FunctionEntry`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ExportKind {
    Native,
    /// Raw MArgument export (`#[export(margs)]`). Argument/return types are
    /// opaque to the macro, so this can never become a working
    /// `LibraryFunctionLoad` call — [`library_function_load`] maps it
    /// straight to `Missing["NotAvailable"]` instead.
    Margs,
    Wstp,
    Wxf,
}

impl ExportKind {
    /// Parse the `kind` string stored in a [`FunctionEntry`]; `None` if unknown.
    fn from_kind_str(s: &str) -> Option<ExportKind> {
        match s {
            "Native" => Some(ExportKind::Native),
            "Margs" => Some(ExportKind::Margs),
            "Wstp" => Some(ExportKind::Wstp),
            "Wxf" => Some(ExportKind::Wxf),
            _ => None,
        }
    }
}

/// The `Caller` wrapper symbol name for one export kind. This is both the head
/// of each rule's value (`NativeCaller[...]`, ...) and the name bound by
/// [`caller_binding`] — used to detect which callers a given association
/// actually references.
fn caller_name(kind: ExportKind) -> &'static str {
    match kind {
        ExportKind::Native => "NativeCaller",
        ExportKind::Wstp => "WSTPCaller",
        ExportKind::Wxf => "WXFCaller",
        // Margs entries are never wrapped in a caller (see
        // `library_function_load`) and `with_callers` never iterates this
        // variant, so this is never reached.
        ExportKind::Margs => unreachable!("margs mode never needs a caller binding"),
    }
}

/// The `Set[Caller, ...]` prelude binding for one export kind. Emitted (only
/// when referenced — see [`with_callers`]) as part of the `With[{...}, <|...|>]`
/// loader so the per-function `Caller[LibraryFunctionLoad[...]]` rules from
/// [`library_function_rule`] resolve.
///
/// - `NativeCaller = Identity` — the loaded function is used directly.
/// - `WSTPCaller` resets `$Context`/`$ContextPath` around the call for
///   predictable symbol resolution across the link.
/// - `WXFCaller` serializes the args and deserializes the result so the WXF
///   `{ByteArray} -> ByteArray` function presents a normal-expression interface.
fn caller_binding(kind: ExportKind) -> Expr {
    match kind {
        ExportKind::Native => expr!(::Set[::NativeCaller, ::Identity]),
        ExportKind::Wstp => expr!(::Set[
            ::WSTPCaller,
            ::Function[::With[
                ::List[::Set[::f, ::Slot[1]]],
                ::Function[::Block[
                    ::List[
                        ::Set[::$Context, "RustLinkWSTPPrivateContext`"],
                        ::Set[::$ContextPath, ::List[]]
                    ],
                    ::f[::SlotSequence[1]]
                ]]
            ]]
        ]),
        ExportKind::Wxf => expr!(::Set[
            ::WXFCaller,
            ::Function[::Composition[
                ::BinaryDeserialize,
                ::Slot[1],
                ::BinarySerialize,
                ::List
            ]]
        ]),
        ExportKind::Margs => unreachable!("margs mode never needs a caller binding"),
    }
}

/// Build `Caller[LibraryFunctionLoad[lib, name, <args>, <ret>]]` for one
/// exported function — every kind always produces a real
/// `LibraryFunctionLoad` call. [`ExportKind::Margs`] carries a real
/// `native_sig` unconditionally too: the macro always materializes one —
/// either the user's `#[export(margs, args = .., ret = ..)]` annotation, or
/// (with a compile-time warning) a default of the same fixed
/// `LinkObject`/`LinkObject` placeholder `ExportKind::Wstp` uses. So `Margs`
/// is handled identically to `Native` here — same wire ABI, same
/// `NativeCaller = Identity` wrapper.
///
/// `lib` is any `Expr` that evaluates to the dylib path — a string literal at
/// runtime, or a `libN` symbol bound in the generated `Functions.wl` prelude.
/// `native_sig` supplies the real argument/return type `Expr`s for
/// [`ExportKind::Native`] and [`ExportKind::Margs`]; for `Wstp`/`Wxf` the
/// wire shape is fixed and it is ignored.
fn library_function_load(
    kind: ExportKind,
    name: &str,
    lib: Expr,
    native_sig: Option<(Vec<Expr>, Expr)>,
) -> Expr {
    // `(arg-type spec, return-type spec)` — the 3rd and 4th positional args to
    // LibraryFunctionLoad. Native/Margs carry the real signature; Wstp passes
    // the bare `LinkObject` marker; Wxf is always
    // `{{ByteArray,"Constant"}} -> ByteArray` ("Constant" is the no-mutate
    // promise that lets the kernel skip a deep copy of the serialized input).
    let (args, ret): (Expr, Expr) = match kind {
        ExportKind::Native | ExportKind::Margs => {
            let (args, ret) =
                native_sig.unwrap_or_else(|| (Vec::new(), Expr::string("")));
            (Expr::from(args), ret)
        },
        ExportKind::Wstp => (expr!(::LinkObject), expr!(::LinkObject)),
        ExportKind::Wxf => (
            expr!(::List[::List[::ByteArray, "Constant"]]),
            expr!(::ByteArray),
        ),
    };
    let load = expr!(::LibraryFunctionLoad[lib, name, args, ret]);
    match kind {
        ExportKind::Native | ExportKind::Margs => expr!(::NativeCaller[load]),
        ExportKind::Wstp => expr!(::WSTPCaller[load]),
        ExportKind::Wxf => expr!(::WXFCaller[load]),
    }
}

/// Wrap a finished association in `With[bindings, assoc]`, emitting ONLY the
/// caller prelude bindings (`NativeCaller`/`WSTPCaller`/`WXFCaller`) actually
/// referenced by the rules, followed by `extra` (e.g. the CLI's
/// `libN = FileNameJoin[...]` path bindings). A loader with no WXF functions, for
/// instance, omits the `WXFCaller` binding entirely.
fn with_callers(extra: Vec<Expr>, assoc: Association) -> Expr {
    let used = |kind: ExportKind| {
        let name = caller_name(kind);
        assoc.iter().any(|entry| {
            // Each rule value is `Caller[LibraryFunctionLoad[...]]`; the head
            // symbol identifies which caller it needs.
            matches!(
                entry.value.normal_head().as_ref().map(Expr::kind),
                Some(ExprKind::Symbol(s)) if s.as_str() == name
            )
        })
    };

    let mut bindings: Vec<Expr> = [ExportKind::Native, ExportKind::Wstp, ExportKind::Wxf]
        .iter()
        .copied()
        .filter(|&kind| used(kind))
        .map(caller_binding)
        .collect();
    bindings.extend(extra);
    expr!(::With[bindings, assoc])
}

/// Compute the association key for an exported function: `"namespace::name"`
/// when a namespace is supplied, otherwise the bare `name`.
fn export_key(namespace: Option<&str>, name: &str) -> String {
    match namespace {
        Some(ns) => format!("{ns}::{name}"),
        None => name.to_owned(),
    }
}

/// One built library to include in a generated paclet loader: where to find the
/// library at load time, plus the functions it exports.
pub struct LibraryArtifact {
    /// A Wolfram Language expression that evaluates to the library file's path
    /// when `Functions.wl` is loaded. The CLI passes something like
    /// `FileNameJoin[{DirectoryName[$InputFileName], "abc123.dylib"}]`; the
    /// runtime passes a plain absolute-path string.
    pub path: Expr,
    /// When `Some(ns)`, every function key is namespaced as `"ns::name"` so
    /// functions from different libraries cannot collide.
    pub namespace: Option<String>,
    /// The functions this library exports, as decoded from its embedded
    /// manifest (see [`FunctionEntry`]).
    pub functions: Vec<FunctionEntry>,
}

/// Build the loader association written to `Functions.wl`, the single entry
/// point shared by the `cargo wl build` CLI and the runtime WSTP loader.
///
/// Produces `With[{<caller prelude…>, lib1 = path1, …}, <|key -> Caller[LibraryFunctionLoad[…]], …|>]`,
/// where each library's `path` is bound to a `libN` symbol that its functions
/// reference. Only the caller-prelude bindings (`NativeCaller`/`WSTPCaller`/
/// `WXFCaller`) actually used are emitted, and libraries with no functions are
/// skipped.
pub fn library_functions_loader(libraries: &[LibraryArtifact]) -> Expr {
    let mut bindings: Vec<Expr> = Vec::new();
    let mut rules: Vec<RuleEntry> = Vec::new();

    let mut n = 0;
    for library in libraries {
        if library.functions.is_empty() {
            continue;
        }
        n += 1;
        let libvar = Symbol::new(&format!("lib{n}"));
        bindings.push(expr!(::Set[(Expr::from(libvar.clone())), (library.path.clone())]));

        for entry in &library.functions {
            let Some(kind) = ExportKind::from_kind_str(&entry.kind) else {
                continue;
            };
            let native_sig = match kind {
                ExportKind::Native | ExportKind::Margs => {
                    Some((entry.params.clone(), entry.ret.clone()))
                },
                _ => None,
            };
            let key = export_key(library.namespace.as_deref(), &entry.name);
            rules.push(RuleEntry::rule(
                Expr::from(key.as_str()),
                library_function_load(
                    kind,
                    &entry.name,
                    Expr::from(libvar.clone()),
                    native_sig,
                ),
            ));
        }
    }

    with_callers(bindings, rules.into_iter().collect())
}

/// Inventory entry for one `#[export]`-marked function.
///
/// Replaces the legacy `LibraryLinkFunction` enum from `wolfram-library-link`.
/// All three export-mode runtimes (`wolfram-export-native`, `wolfram-export-wstp`,
/// `wolfram-export-wxf`) submit entries of this single shared type to one
/// global inventory; [`exported_library_functions_association`] iterates that
/// inventory regardless of mode.
pub enum ExportEntry {
    /// Native MArgument-based export.
    Native {
        /// Exported symbol name (matches the `#[no_mangle] extern "C"` symbol).
        name: &'static str,
        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s.
        ///
        /// See the implementation note on `LibraryLinkFunction::Native::signature`
        /// for why this is a `fn` pointer rather than a `Box<dyn ...>`.
        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
    },
    /// Raw MArgument-based export (`#[export(margs)]`). Argument/return types
    /// are opaque to the macro by default (the user marshals
    /// `&[MArgument]`/`MArgument` by hand) — unlike `Native`, whose signature
    /// is inferred from `FromArg`/`IntoArg` impls, there's nothing to infer
    /// here. `signature` is never absent, though: the macro always
    /// materializes one at expansion time — the user's
    /// `#[export(margs, args = ..., ret = ...)]` annotation if given, or
    /// (with a compile-time warning) a default of the same fixed
    /// `LinkObject`/`LinkObject` placeholder `Wstp` uses.
    Margs {
        /// Exported symbol name.
        name: &'static str,
        /// (arg types, return type) — user-declared, or the macro's own
        /// `LinkObject`/`LinkObject` default.
        signature: fn() -> (Vec<Expr>, Expr),
    },
    /// WSTP `LinkObject`-based export.
    Wstp {
        /// Exported symbol name.
        name: &'static str,
    },
    /// Typed-args WXF-based export (NEW). Wire shape is `{ByteArray} -> ByteArray`
    /// at the LibraryLink level; the byte arrays carry WXF-encoded payloads of
    /// the user-declared Rust types.
    Wxf {
        /// Exported symbol name.
        name: &'static str,
        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s
        /// — used for the manifest's typed signature display, not for the WL-side
        /// `LibraryFunctionLoad` call (which is always `{ByteArray} -> ByteArray`).
        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
    },
}

#[cfg(feature = "automate-function-loading-boilerplate")]
inventory::collect!(ExportEntry);

//==============================================================================
// __wolfram_manifest__: build-time-extractable manifest symbol
//==============================================================================

/// C-ABI symbol called via `dlopen` at build time to extract the library's
/// exported-function manifest without running a WSTP loop.
///
/// Returns a pointer to a leaked buffer whose first 8 bytes are the WXF payload
/// length as a little-endian `u64`, followed immediately by the WXF-serialized
/// `Vec<FunctionEntry>`. Deserialize with:
/// ```ignore
/// let len = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
/// wolfram_serialize::deserialize::<Vec<FunctionEntry>>(&buf[8..8+len], None)
/// ```
#[cfg(feature = "automate-function-loading-boilerplate")]
#[no_mangle]
pub extern "C" fn __wolfram_manifest__() -> *const u8 {
    let entries: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
        .filter_map(ExportEntry::to_function_entry)
        .collect();

    let wxf = wolfram_serialize::to_wxf(&entries, None)
        .expect("manifest WXF serialization failed");
    // Prepend the payload length as 8 little-endian bytes so the caller needs
    // no out-parameter — one zero-arg call, read [0..8] for the length, [8..] for WXF.
    let mut buf = Vec::with_capacity(8 + wxf.len());
    buf.extend_from_slice(&(wxf.len() as u64).to_le_bytes());
    buf.extend_from_slice(&wxf);
    Box::leak(buf.into_boxed_slice()).as_ptr()
}

/// Returns an [`Association`][Association] containing the names and `LibraryFunctionLoad`
/// calls for every `#[export(..)]`-marked function in this library.
///
/// Iterates the shared inventory built up by `inventory::submit!` calls from
/// the three export-mode runtimes. Same Association shape today's
/// `wolfram-library-link::exported_library_functions_association` produces,
/// plus an extra arm for the new `Wxf` mode.
///
/// `library` overrides automatic dylib path detection.
///
/// [Association]: https://reference.wolfram.com/language/ref/Association.html
#[cfg(feature = "automate-function-loading-boilerplate")]
pub fn exported_library_functions_association(
    library: Option<std::path::PathBuf>,
) -> Expr {
    let library: std::path::PathBuf = library.unwrap_or_else(|| {
        process_path::get_dylib_path()
            .expect("unable to automatically determine Rust LibraryLink dynamic library file path. Suggestion: pass the library name or path to exported_library_functions_association(..)")
    });
    let library = library
        .to_str()
        .expect("unable to convert library file path to str");

    // A single dylib's inventory has no cross-library clashes, so no
    // namespacing; the runtime loads from the resolved absolute path.
    let functions: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
        .filter_map(ExportEntry::to_function_entry)
        .collect();
    library_functions_loader(&[LibraryArtifact {
        path: Expr::string(library),
        namespace: None,
        functions,
    }])
}

#[cfg(feature = "automate-function-loading-boilerplate")]
impl ExportEntry {
    /// Convert an inventory entry into a serializable [`FunctionEntry`].
    ///
    /// Returns `None` for a `Native` function whose signature cannot be
    /// resolved (e.g. an unsupported argument type) — it could not be loaded,
    /// so it is omitted from both the manifest and the loader. `Wstp`/`Wxf`
    /// have a fixed wire shape and carry empty `params`/`ret` placeholders.
    /// `Margs`'s `signature` never fails to resolve — the macro always
    /// materializes one, whether from the user's annotation or its own
    /// `LinkObject`/`LinkObject` default — so it's never omitted either.
    fn to_function_entry(&self) -> Option<FunctionEntry> {
        Some(match self {
            ExportEntry::Native { name, signature } => {
                let (params, ret) = signature().ok()?;
                FunctionEntry {
                    name: (*name).to_owned(),
                    kind: "Native".to_owned(),
                    params,
                    ret,
                }
            },
            ExportEntry::Margs { name, signature } => {
                let (params, ret) = signature();
                FunctionEntry {
                    name: (*name).to_owned(),
                    kind: "Margs".to_owned(),
                    params,
                    ret,
                }
            },
            ExportEntry::Wstp { name } => FunctionEntry {
                name: (*name).to_owned(),
                kind: "Wstp".to_owned(),
                params: vec![],
                ret: Expr::string(""),
            },
            ExportEntry::Wxf { name, .. } => FunctionEntry {
                name: (*name).to_owned(),
                kind: "Wxf".to_owned(),
                params: vec![],
                ret: Expr::string(""),
            },
        })
    }
}