Skip to main content

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}