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, Symbol};
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#[derive(ToWXF, FromWXF, Debug)]
26#[allow(missing_docs)]
27pub struct FunctionEntry {
28    pub name: String,
29    pub kind: String,
30    pub params: Vec<String>,
31    pub ret: String,
32}
33
34/// Inventory entry for one `#[export]`-marked function.
35///
36/// Replaces the legacy `LibraryLinkFunction` enum from `wolfram-library-link`.
37/// All three export-mode runtimes (`wolfram-export-native`, `wolfram-export-wstp`,
38/// `wolfram-export-wxf`) submit entries of this single shared type to one
39/// global inventory; [`exported_library_functions_association`] iterates that
40/// inventory regardless of mode.
41pub enum ExportEntry {
42    /// Native MArgument-based export.
43    Native {
44        /// Exported symbol name (matches the `#[no_mangle] extern "C"` symbol).
45        name: &'static str,
46        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s.
47        ///
48        /// See the implementation note on `LibraryLinkFunction::Native::signature`
49        /// for why this is a `fn` pointer rather than a `Box<dyn ...>`.
50        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
51    },
52    /// WSTP `LinkObject`-based export.
53    Wstp {
54        /// Exported symbol name.
55        name: &'static str,
56    },
57    /// Typed-args WXF-based export (NEW). Wire shape is `{ByteArray} -> ByteArray`
58    /// at the LibraryLink level; the byte arrays carry WXF-encoded payloads of
59    /// the user-declared Rust types.
60    Wxf {
61        /// Exported symbol name.
62        name: &'static str,
63        /// Closure returning (arg types, return type) as Wolfram Language `Expr`s
64        /// — used for the manifest's typed signature display, not for the WL-side
65        /// `LibraryFunctionLoad` call (which is always `{ByteArray} -> ByteArray`).
66        signature: fn() -> Result<(Vec<Expr>, Expr), String>,
67    },
68}
69
70#[cfg(feature = "automate-function-loading-boilerplate")]
71inventory::collect!(ExportEntry);
72
73//==============================================================================
74// __wolfram_manifest__: build-time-extractable manifest symbol
75//==============================================================================
76
77/// C-ABI symbol that the `cargo wolfram-manifest` subcommand calls via `dlopen`
78/// to extract the library's exported-function manifest at build time, without
79/// running a WSTP loop.
80///
81/// Returns a pointer to a leaked, statically-typed WXF byte buffer of the
82/// manifest Association; the caller writes `*out_len` with the length. The
83/// returned buffer must NOT be freed by the caller (it lives for the rest
84/// of the process — manifests are small and called at most once per build).
85///
86/// The manifest content is identical to what `exported_library_functions_association(None)`
87/// would produce at runtime over WSTP — same Association[name -> LibraryFunctionLoad[...]]
88/// shape, just serialized as WXF bytes for an out-of-band, language-agnostic
89/// consumer.
90#[cfg(feature = "automate-function-loading-boilerplate")]
91#[no_mangle]
92pub extern "C" fn __wolfram_manifest__(out_len: *mut usize) -> *const u8 {
93    let assoc: Expr = exported_library_functions_association(None);
94    let bytes: Vec<u8> =
95        wolfram_serialize::to_wxf(&assoc, None).expect("manifest WXF serialization");
96    // Leak the buffer so the pointer remains valid after this function returns.
97    // The manifest is small and the caller (cargo-wolfram-manifest) only calls
98    // this once per build.
99    let len = bytes.len();
100    let ptr = Box::leak(bytes.into_boxed_slice()).as_ptr();
101    unsafe {
102        *out_len = len;
103    }
104    ptr
105}
106
107/// C-ABI symbol returning WXF-serialized `Vec<FunctionEntry>` for every exported
108/// function. Consumed by `cargo wl build` via `libloading` — no WL kernel needed.
109///
110/// Returns a pointer to a leaked buffer whose first 8 bytes are the WXF payload
111/// length as a little-endian `u64`, followed immediately by the WXF bytes.
112/// Deserialize with:
113/// ```ignore
114/// let len = u64::from_le_bytes(buf[..8].try_into().unwrap()) as usize;
115/// wolfram_serialize::deserialize::<Vec<FunctionEntry>>(&buf[8..8+len], None)
116/// ```
117#[cfg(feature = "automate-function-loading-boilerplate")]
118#[no_mangle]
119pub extern "C" fn __wolfram_manifest_data__() -> *const u8 {
120    let entries: Vec<FunctionEntry> = inventory::iter::<ExportEntry>()
121        .map(|e| match e {
122            ExportEntry::Native { name, signature } => {
123                let (params, ret) =
124                    signature().unwrap_or_else(|_| (vec![], Expr::string("")));
125                FunctionEntry {
126                    name: (*name).to_owned(),
127                    kind: "Native".to_owned(),
128                    params: params.iter().map(|e| e.to_string()).collect(),
129                    ret: ret.to_string(),
130                }
131            },
132            ExportEntry::Wstp { name } => FunctionEntry {
133                name: (*name).to_owned(),
134                kind: "Wstp".to_owned(),
135                params: vec![],
136                ret: String::new(),
137            },
138            ExportEntry::Wxf { name, .. } => FunctionEntry {
139                name: (*name).to_owned(),
140                kind: "Wxf".to_owned(),
141                params: vec![],
142                ret: String::new(),
143            },
144        })
145        .collect();
146
147    let wxf =
148        wolfram_serialize::to_wxf(&entries, None).expect("manifest WXF serialization failed");
149    // Prepend the payload length as 8 little-endian bytes so the caller needs
150    // no out-parameter — one zero-arg call, read [0..8] for the length, [8..] for WXF.
151    let mut buf = Vec::with_capacity(8 + wxf.len());
152    buf.extend_from_slice(&(wxf.len() as u64).to_le_bytes());
153    buf.extend_from_slice(&wxf);
154    Box::leak(buf.into_boxed_slice()).as_ptr()
155}
156
157/// Returns an [`Association`][Association] containing the names and `LibraryFunctionLoad`
158/// calls for every `#[export(..)]`-marked function in this library.
159///
160/// Iterates the shared inventory built up by `inventory::submit!` calls from
161/// the three export-mode runtimes. Same Association shape today's
162/// `wolfram-library-link::exported_library_functions_association` produces,
163/// plus an extra arm for the new `Wxf` mode.
164///
165/// `library` overrides automatic dylib path detection.
166///
167/// [Association]: https://reference.wolfram.com/language/ref/Association.html
168#[cfg(feature = "automate-function-loading-boilerplate")]
169pub fn exported_library_functions_association(
170    library: Option<std::path::PathBuf>,
171) -> Expr {
172    let library: std::path::PathBuf = library.unwrap_or_else(|| {
173        process_path::get_dylib_path()
174            .expect("unable to automatically determine Rust LibraryLink dynamic library file path. Suggestion: pass the library name or path to exported_library_functions_association(..)")
175    });
176
177    use wolfram_expr::{Association, RuleEntry};
178    let assoc: Association = inventory::iter::<ExportEntry>()
179        .filter_map(|entry| {
180            let code = entry.loading_code(&library).ok()?;
181            Some(RuleEntry::rule(Expr::from(entry.name()), code))
182        })
183        .collect();
184    Expr::from(assoc)
185}
186
187#[cfg_attr(
188    not(feature = "automate-function-loading-boilerplate"),
189    allow(dead_code)
190)]
191impl ExportEntry {
192    fn name(&self) -> &str {
193        match self {
194            ExportEntry::Native { name, .. } => name,
195            ExportEntry::Wstp { name } => name,
196            ExportEntry::Wxf { name, .. } => name,
197        }
198    }
199
200    fn loading_code(&self, library: &std::path::PathBuf) -> Result<Expr, String> {
201        use wolfram_expr::expr;
202
203        let library = library
204            .to_str()
205            .expect("unable to convert library file path to str");
206
207        let code = match self {
208            ExportEntry::Native { name, signature } => {
209                let (args, ret) = signature()?;
210                let name = *name;
211                expr!(System::LibraryFunctionLoad[library, name, args, ret])
212            },
213            // WSTP-mode: wraps LibraryFunctionLoad in Function[Block[...]] that
214            // resets $Context for predictable symbol resolution across the link.
215            ExportEntry::Wstp { name } => {
216                let name = *name;
217                let load_call = expr!(
218                    System::LibraryFunctionLoad[library, name, "LinkObject", "LinkObject"]
219                );
220                let var = expr!(RustLink::Private::wstpFunc);
221                // `$Context` / `$ContextPath` can't be `::`-idents (`$` isn't a
222                // Rust ident char), so build those symbols from strings.
223                let ctx = Expr::from(Symbol::new("System`$Context"));
224                let ctx_path = Expr::from(Symbol::new("System`$ContextPath"));
225                let var2 = var.clone();
226                let body = expr!(var[System::SlotSequence[1]]);
227                expr!(System::With[
228                    System::List[System::Set[var2, load_call]],
229                    System::Function[System::Block[
230                        System::List[
231                            System::Set[ctx, "RustLinkWSTPPrivateContext`"],
232                            System::Set[ctx_path, System::List[]]
233                        ],
234                        body
235                    ]]
236                ])
237            },
238            // Wxf-mode: wire shape is always {ByteArray} -> ByteArray.
239            ExportEntry::Wxf { name, .. } => {
240                let name = *name;
241                expr!(System::LibraryFunctionLoad[library, name, System::List["ByteArray"], "ByteArray"])
242            },
243        };
244
245        Ok(code)
246    }
247}