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}