palladium_plugin/wasm.rs
1//! WASM plugin traits for compiling, instantiating, and calling WASM modules.
2//!
3//! These traits abstract over a concrete WASM runtime (e.g. `wasmtime`) so
4//! the plugin host can work with any backend. The `pd-wasm-wasmtime` crate
5//! provides the production implementation via `WasmtimeHost`.
6//!
7//! # Design notes
8//!
9//! * [`WasmImports`] closures have `+ Send + Sync + 'static` bounds so they
10//! can be registered as wasmtime host functions, which require `Send + Sync`.
11//! `WasmActor` (Phase 5.6) uses `Arc<Mutex<…>>` for the outbox buffer.
12//! * [`WasmModule`] exposes `as_any()` so the concrete `WasmHost` can downcast
13//! the `Box<dyn WasmModule>` it created back to its own type without a
14//! separate runtime registry.
15
16use std::any::Any;
17use std::fmt;
18
19// ── WasmError ─────────────────────────────────────────────────────────────────
20
21/// Errors that can occur during WASM module compilation, instantiation, or
22/// execution.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum WasmError {
25 /// The WASM bytes failed to compile (parse, validate, or codegen error).
26 Compile(String),
27 /// The module could not be instantiated (e.g., missing import, link error).
28 Instantiation(String),
29 /// A host function call failed at runtime.
30 Call(String),
31 /// Reading from or writing to WASM linear memory failed (bounds check).
32 MemoryAccess,
33 /// The WASM module consumed all available fuel and was stopped.
34 FuelExhausted,
35 /// The WASM module trapped (unreachable, integer divide-by-zero, etc.).
36 Trap(String),
37}
38
39impl fmt::Display for WasmError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Compile(s) => write!(f, "WASM compile error: {s}"),
43 Self::Instantiation(s) => write!(f, "WASM instantiation error: {s}"),
44 Self::Call(s) => write!(f, "WASM call error: {s}"),
45 Self::MemoryAccess => write!(f, "WASM memory access out of bounds"),
46 Self::FuelExhausted => write!(f, "WASM fuel budget exhausted"),
47 Self::Trap(s) => write!(f, "WASM trap: {s}"),
48 }
49 }
50}
51
52impl std::error::Error for WasmError {}
53
54// ── WasmVal ───────────────────────────────────────────────────────────────────
55
56/// A WASM numeric value, used for function call arguments and return values.
57///
58/// `F32` and `F64` are stored as their bit representations (`u32` and `u64`)
59/// so that `WasmVal` can derive `Eq` and remain free of IEEE754 float
60/// comparison edge cases.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum WasmVal {
63 I32(i32),
64 I64(i64),
65 /// IEEE 754 single-precision bits.
66 F32(u32),
67 /// IEEE 754 double-precision bits.
68 F64(u64),
69}
70
71// ── WasmImports ───────────────────────────────────────────────────────────────
72
73/// Host function callbacks injected into a WASM module at instantiation.
74///
75/// These correspond to the `pd_*` imports that a plugin WASM module may call.
76/// The closures receive raw pointers into a buffer that was read from WASM
77/// linear memory immediately before the call; the pointers are valid for the
78/// duration of the call only.
79///
80/// The `+ Send + Sync + 'static` bounds are required because wasmtime host
81/// functions must be thread-safe. Use [`Default`] to obtain no-op stubs.
82pub struct WasmImports {
83 /// Send a message to another actor.
84 ///
85 /// Arguments: `(envelope_ptr, envelope_len, payload_ptr, payload_len)`.
86 /// Returns 0 on success, negative on error.
87 pub pd_send: Box<dyn Fn(*const u8, u32, *const u8, u32) -> i32 + Send + Sync + 'static>,
88
89 /// Return the current engine time in microseconds.
90 pub pd_now_micros: Box<dyn Fn() -> u64 + Send + Sync + 'static>,
91
92 /// Emit a log message.
93 ///
94 /// Arguments: `(level, msg_ptr, msg_len)`.
95 pub pd_log: Box<dyn Fn(i32, *const u8, u32) + Send + Sync + 'static>,
96}
97
98impl Default for WasmImports {
99 /// No-op stubs — suitable for tests that don't exercise host imports.
100 fn default() -> Self {
101 Self {
102 pd_send: Box::new(|_, _, _, _| 0),
103 pd_now_micros: Box::new(|| 0),
104 pd_log: Box::new(|_, _, _| {}),
105 }
106 }
107}
108
109// ── WasmModule ────────────────────────────────────────────────────────────────
110
111/// A compiled (but not yet instantiated) WASM module.
112///
113/// Created by [`WasmHost::compile`]. Implementors must also expose `as_any()`
114/// so the host can downcast `&dyn WasmModule` back to its concrete type during
115/// [`WasmHost::instantiate`].
116///
117/// `Send + Sync` are required so compiled modules can be wrapped in `Arc` and
118/// shared across actor factory closures (which must be `Send + Sync + 'static`).
119pub trait WasmModule: Send + Sync + 'static {
120 /// Names of all exports declared by the module.
121 fn exports(&self) -> Vec<String>;
122
123 /// Downcast support — implementations should return `self`.
124 fn as_any(&self) -> &dyn Any;
125}
126
127// ── WasmInstance ──────────────────────────────────────────────────────────────
128
129/// A fully instantiated WASM module, ready to execute exported functions.
130///
131/// Created by [`WasmHost::instantiate`].
132pub trait WasmInstance: 'static {
133 /// Call an exported function by name.
134 ///
135 /// Returns `Err(WasmError::FuelExhausted)` if the module ran out of fuel,
136 /// `Err(WasmError::Trap(…))` for other traps, and
137 /// `Err(WasmError::Call(…))` for missing exports or type errors.
138 fn call(&mut self, func: &str, args: &[WasmVal]) -> Result<Vec<WasmVal>, WasmError>;
139
140 /// Read `len` bytes from WASM linear memory at `offset`.
141 ///
142 /// Returns `Err(WasmError::MemoryAccess)` on bounds violation.
143 fn memory_read(&self, offset: u32, len: u32) -> Result<Vec<u8>, WasmError>;
144
145 /// Write `data` into WASM linear memory starting at `offset`.
146 ///
147 /// Returns `Err(WasmError::MemoryAccess)` on bounds violation.
148 fn memory_write(&mut self, offset: u32, data: &[u8]) -> Result<(), WasmError>;
149
150 /// Set the fuel budget for subsequent calls.
151 ///
152 /// Has no effect if the runtime was not configured with fuel consumption
153 /// enabled; in that case returns `Ok(())` silently.
154 fn set_fuel(&mut self, fuel: u64) -> Result<(), WasmError>;
155
156 /// Return the number of fuel units consumed so far by this instance.
157 ///
158 /// Returns `None` if the runtime was not configured with fuel tracking.
159 fn fuel_consumed(&self) -> Option<u64>;
160}
161
162// ── WasmHost ──────────────────────────────────────────────────────────────────
163
164/// Factory for compiling and instantiating WASM modules.
165///
166/// Implementors wrap a concrete WASM runtime (e.g. `WasmtimeHost`).
167/// `Send + Sync + 'static` because a single host may be shared across
168/// multiple threads (e.g., multiple engine cores each loading their own
169/// plugin actors from the same compiled module).
170pub trait WasmHost: Send + Sync + 'static {
171 /// Compile `wasm` bytes into a reusable module.
172 fn compile(&self, wasm: &[u8]) -> Result<Box<dyn WasmModule>, WasmError>;
173
174 /// Instantiate a previously compiled module, registering `imports` as
175 /// host functions.
176 fn instantiate(
177 &self,
178 module: &dyn WasmModule,
179 imports: WasmImports,
180 ) -> Result<Box<dyn WasmInstance>, WasmError>;
181}