1#![cfg(feature = "wasmtime-runtime")]
23
24use std::path::Path;
25use std::sync::Mutex;
26
27use wasmtime::{Engine, Instance, Module, Store, TypedFunc};
28
29use crate::error::{KernelError, Result};
30
31pub struct WasmExecutor {
34 engine: Engine,
35 module: Module,
36 inner: Mutex<Instance>,
37 store: Mutex<Store<()>>,
38}
39
40impl WasmExecutor {
41 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
43 let bytes = std::fs::read(path.as_ref()).map_err(|e| {
44 KernelError::Other(anyhow::anyhow!(
45 "failed to read wasm artefact {}: {e}",
46 path.as_ref().display()
47 ))
48 })?;
49 Self::from_bytes(&bytes)
50 }
51
52 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
54 let engine = Engine::default();
55 let module = Module::new(&engine, bytes).map_err(to_kernel)?;
56 let mut store: Store<()> = Store::new(&engine, ());
57 let instance = Instance::new(&mut store, &module, &[]).map_err(to_kernel)?;
58 Ok(Self {
59 engine,
60 module,
61 inner: Mutex::new(instance),
62 store: Mutex::new(store),
63 })
64 }
65
66 pub fn list_exports(&self) -> Vec<String> {
68 self.module
69 .exports()
70 .filter_map(|e| match e.ty() {
71 wasmtime::ExternType::Func(_) => Some(e.name().to_string()),
72 _ => None,
73 })
74 .collect()
75 }
76
77 pub fn call_i32_to_i32(&self, name: &str, arg: i32) -> Result<i32> {
79 let mut store = self.store.lock().unwrap();
80 let instance = self.inner.lock().unwrap();
81 let func: TypedFunc<i32, i32> = instance
82 .get_typed_func::<i32, i32>(&mut *store, name)
83 .map_err(|e| {
84 KernelError::Other(anyhow::anyhow!(
85 "wasm export `{name}` is not (i32) -> i32: {e}"
86 ))
87 })?;
88 func.call(&mut *store, arg).map_err(to_kernel)
89 }
90
91 pub fn call_json(&self, method: &str, input: &serde_json::Value) -> Result<serde_json::Value> {
105 let mut store = self.store.lock().unwrap();
106 let instance = self.inner.lock().unwrap();
107
108 let alloc: TypedFunc<i32, i32> = instance
110 .get_typed_func::<i32, i32>(&mut *store, "alloc")
111 .map_err(|e| KernelError::Other(anyhow::anyhow!("alloc export: {e}")))?;
112 let free: TypedFunc<(i32, i32), ()> = instance
113 .get_typed_func::<(i32, i32), ()>(&mut *store, "free")
114 .map_err(|e| KernelError::Other(anyhow::anyhow!("free export: {e}")))?;
115 let invoke: TypedFunc<(i32, i32, i32, i32), i64> = instance
116 .get_typed_func::<(i32, i32, i32, i32), i64>(&mut *store, "oxide_invoke")
117 .map_err(|e| KernelError::Other(anyhow::anyhow!("oxide_invoke export: {e}")))?;
118
119 let memory = instance
120 .get_memory(&mut *store, "memory")
121 .ok_or_else(|| KernelError::Other(anyhow::anyhow!("no `memory` export")))?;
122
123 let method_bytes = method.as_bytes().to_vec();
125 let input_bytes = serde_json::to_vec(input).map_err(|e| KernelError::Other(e.into()))?;
126
127 let method_len = method_bytes.len() as i32;
129 let method_ptr = alloc
130 .call(&mut *store, method_len)
131 .map_err(|e| KernelError::Other(anyhow::anyhow!("alloc method: {e}")))?;
132 {
133 let mem = memory.data_mut(&mut *store);
134 let s = method_ptr as usize;
135 if s + method_bytes.len() > mem.len() {
136 return Err(KernelError::Other(anyhow::anyhow!("method OOB")));
137 }
138 mem[s..s + method_bytes.len()].copy_from_slice(&method_bytes);
139 }
140
141 let input_len = input_bytes.len() as i32;
143 let input_ptr = alloc
144 .call(&mut *store, input_len)
145 .map_err(|e| KernelError::Other(anyhow::anyhow!("alloc input: {e}")))?;
146 {
147 let mem = memory.data_mut(&mut *store);
148 let s = input_ptr as usize;
149 if s + input_bytes.len() > mem.len() {
150 return Err(KernelError::Other(anyhow::anyhow!("input OOB")));
151 }
152 mem[s..s + input_bytes.len()].copy_from_slice(&input_bytes);
153 }
154
155 let result = invoke
157 .call(&mut *store, (method_ptr, method_len, input_ptr, input_len))
158 .map_err(|e| KernelError::Other(anyhow::anyhow!("oxide_invoke: {e}")))?;
159
160 let _ = free.call(&mut *store, (method_ptr, method_len));
162 let _ = free.call(&mut *store, (input_ptr, input_len));
163
164 let out_ptr = ((result >> 32) & 0xFFFF_FFFF) as usize;
166 let out_len = (result & 0xFFFF_FFFF) as usize;
167
168 let output_bytes = {
169 let mem = memory.data(&*store);
170 if out_ptr + out_len > mem.len() {
171 return Err(KernelError::Other(anyhow::anyhow!("output OOB")));
172 }
173 mem[out_ptr..out_ptr + out_len].to_vec()
174 };
175
176 let _ = free.call(&mut *store, (out_ptr as i32, out_len as i32));
178
179 let output = serde_json::from_slice(&output_bytes)
180 .map_err(|e| KernelError::Other(anyhow::anyhow!("output JSON: {e}")))?;
181 Ok(output)
182 }
183
184 pub fn engine(&self) -> &Engine {
187 &self.engine
188 }
189}
190
191fn to_kernel(err: impl std::fmt::Display) -> KernelError {
192 KernelError::Other(anyhow::anyhow!("wasmtime: {err}"))
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn add_one_wat() -> Vec<u8> {
202 wat::parse_str(
203 r#"
204 (module
205 (func (export "add_one") (param i32) (result i32)
206 local.get 0
207 i32.const 1
208 i32.add))
209 "#,
210 )
211 .expect("valid wat")
212 }
213
214 #[test]
215 fn executor_can_run_pure_i32_function() {
216 let bytes = add_one_wat();
217 let exec = WasmExecutor::from_bytes(&bytes).unwrap();
218 assert_eq!(exec.list_exports(), vec!["add_one".to_string()]);
219 assert_eq!(exec.call_i32_to_i32("add_one", 41).unwrap(), 42);
220 }
221
222 #[test]
223 fn missing_export_errors() {
224 let exec = WasmExecutor::from_bytes(&add_one_wat()).unwrap();
225 let err = exec.call_i32_to_i32("nope", 1).unwrap_err();
226 assert!(format!("{err}").contains("nope"));
227 }
228
229 fn echo_abi_wat() -> Vec<u8> {
232 wat::parse_str(
237 r#"
238 (module
239 (memory (export "memory") 2)
240
241 ;; Bump allocator: ptr stored at byte 0 (i32), initial = 256
242 (func (export "alloc") (param $size i32) (result i32)
243 (local $ptr i32)
244 ;; read current bump ptr (stored at address 0)
245 (local.set $ptr (i32.load (i32.const 0)))
246 ;; if zero, initialise to 256
247 (if (i32.eqz (local.get $ptr))
248 (then (local.set $ptr (i32.const 256)))
249 )
250 ;; store advanced ptr
251 (i32.store (i32.const 0) (i32.add (local.get $ptr) (local.get $size)))
252 ;; return old ptr
253 (local.get $ptr)
254 )
255
256 ;; free is a no-op in this bump allocator
257 (func (export "free") (param $ptr i32) (param $size i32))
258
259 ;; oxide_invoke: writes {"ok":true} into memory and returns ptr<<32|len
260 (func (export "oxide_invoke")
261 (param $mp i32) (param $ml i32)
262 (param $ip i32) (param $il i32)
263 (result i64)
264 (local $out_ptr i32)
265 (local $payload_len i32)
266 ;; static output: write `{"ok":true}` at address 8192
267 ;; 0x7b = '{', 0x22 = '"', 0x6f='o',0x6b='k',0x22='"',0x3a=':',
268 ;; 0x74='t',0x72='r',0x75='u',0x65='e',0x7d='}' = 11 bytes
269 (i32.store8 (i32.const 8192) (i32.const 123)) ;; {
270 (i32.store8 (i32.const 8193) (i32.const 34)) ;; "
271 (i32.store8 (i32.const 8194) (i32.const 111)) ;; o
272 (i32.store8 (i32.const 8195) (i32.const 107)) ;; k
273 (i32.store8 (i32.const 8196) (i32.const 34)) ;; "
274 (i32.store8 (i32.const 8197) (i32.const 58)) ;; :
275 (i32.store8 (i32.const 8198) (i32.const 116)) ;; t
276 (i32.store8 (i32.const 8199) (i32.const 114)) ;; r
277 (i32.store8 (i32.const 8200) (i32.const 117)) ;; u
278 (i32.store8 (i32.const 8201) (i32.const 101)) ;; e
279 (i32.store8 (i32.const 8202) (i32.const 125)) ;; }
280 (local.set $out_ptr (i32.const 8192))
281 (local.set $payload_len (i32.const 11))
282 ;; return (ptr << 32) | len as i64
283 (i64.or
284 (i64.shl (i64.extend_i32_u (local.get $out_ptr)) (i64.const 32))
285 (i64.extend_i32_u (local.get $payload_len))
286 )
287 )
288 )
289 "#,
290 )
291 .expect("valid echo ABI wat")
292 }
293
294 #[test]
295 fn call_json_round_trips_via_abi() {
296 let bytes = echo_abi_wat();
297 let exec = WasmExecutor::from_bytes(&bytes).unwrap();
298 let result = exec
299 .call_json("echo", &serde_json::json!({"hello": "world"}))
300 .unwrap();
301 assert_eq!(result["ok"], serde_json::Value::Bool(true));
302 }
303}