stormchaser_engine/
wasm.rs1use anyhow::{Context, Result};
2use serde_json::Value;
3use wasmtime::*;
4
5pub struct WasmExecutor {
7 engine: Engine,
8}
9
10impl Default for WasmExecutor {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl WasmExecutor {
17 pub fn new() -> Self {
19 let config = Config::new();
20 let engine = Engine::new(&config).expect("Failed to create Wasmtime engine");
22 Self { engine }
23 }
24
25 pub async fn execute(
27 &self,
28 module_path: &str,
29 function_name: &str,
30 input: Value,
31 ) -> Result<Value> {
32 let module_bytes = if module_path.starts_with("http://")
33 || module_path.starts_with("https://")
34 {
35 reqwest::get(module_path)
36 .await
37 .with_context(|| format!("Failed to fetch WASM module from {}", module_path))?
38 .bytes()
39 .await
40 .with_context(|| format!("Failed to read bytes from {}", module_path))?
41 .to_vec()
42 } else if let Some(base64_data) = module_path.strip_prefix("base64://") {
43 use base64::{engine::general_purpose, Engine as _};
44 general_purpose::STANDARD
45 .decode(base64_data)
46 .with_context(|| "Failed to decode base64 WASM module")?
47 } else {
48 anyhow::bail!("Local file system access for WASM modules is disabled for security reasons. Use http(s):// or base64://");
49 };
50
51 let module = Module::from_binary(&self.engine, &module_bytes)?;
52 let linker = Linker::new(&self.engine);
53
54 let mut store = Store::new(&self.engine, ());
58 let instance = linker.instantiate_async(&mut store, &module).await?;
59
60 let func = instance.get_typed_func::<(i32, i32), i32>(&mut store, function_name)?;
61
62 let memory = instance
72 .get_memory(&mut store, "memory")
73 .context("WASM module must export 'memory'")?;
74
75 let input_str = serde_json::to_string(&input)?;
76 let input_bytes = input_str.as_bytes();
77
78 let alloc = instance.get_typed_func::<i32, i32>(&mut store, "alloc")?;
80 let dealloc = instance.get_typed_func::<(i32, i32), ()>(&mut store, "dealloc")?;
81
82 let ptr = alloc
83 .call_async(&mut store, input_bytes.len() as i32)
84 .await?;
85 memory.write(&mut store, ptr as usize, input_bytes)?;
86
87 let result_ptr_ptr = func
88 .call_async(&mut store, (ptr, input_bytes.len() as i32))
89 .await?;
90
91 let get_len = instance.get_typed_func::<(), i32>(&mut store, "get_result_len")?;
97 let result_len = get_len.call_async(&mut store, ()).await?;
98
99 let mut result_bytes = vec![0u8; result_len as usize];
100 memory.read(&mut store, result_ptr_ptr as usize, &mut result_bytes)?;
101
102 let result_str = String::from_utf8(result_bytes)?;
103 let result_val: Value = serde_json::from_str(&result_str)?;
104
105 dealloc
107 .call_async(&mut store, (ptr, input_bytes.len() as i32))
108 .await?;
109
110 Ok(result_val)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use serde_json::json;
118
119 #[tokio::test]
120 async fn test_wasm_execution() -> Result<()> {
121 let wat = r#"
123 (module
124 (memory (export "memory") 1)
125 (global $result_len (mut i32) (i32.const 0))
126 (global $result_ptr (mut i32) (i32.const 0))
127
128 (func (export "alloc") (param $len i32) (result i32)
129 (i32.const 1024) ;; Simple static allocation for test
130 )
131
132 (func (export "dealloc") (param $ptr i32) (param $len i32)
133 ;; No-op for test
134 )
135
136 (func (export "get_result_len") (result i32)
137 (global.get $result_len)
138 )
139
140 (func (export "run") (param $ptr i32) (param $len i32) (result i32)
141 ;; We just write a fixed JSON response for simplicity in this test
142 (global.set $result_ptr (i32.const 2048))
143 (global.set $result_len (i32.const 16))
144
145 ;; Write '{"echoed": true}' to 2048
146 (i32.store8 (i32.const 2048) (i32.const 123)) ;; {
147 (i32.store8 (i32.const 2049) (i32.const 34)) ;; "
148 (i32.store8 (i32.const 2050) (i32.const 101)) ;; e
149 (i32.store8 (i32.const 2051) (i32.const 99)) ;; c
150 (i32.store8 (i32.const 2052) (i32.const 104)) ;; h
151 (i32.store8 (i32.const 2053) (i32.const 111)) ;; o
152 (i32.store8 (i32.const 2054) (i32.const 101)) ;; e
153 (i32.store8 (i32.const 2055) (i32.const 100)) ;; d
154 (i32.store8 (i32.const 2056) (i32.const 34)) ;; "
155 (i32.store8 (i32.const 2057) (i32.const 58)) ;; :
156 (i32.store8 (i32.const 2058) (i32.const 32)) ;; space
157 (i32.store8 (i32.const 2059) (i32.const 116)) ;; t
158 (i32.store8 (i32.const 2060) (i32.const 114)) ;; r
159 (i32.store8 (i32.const 2061) (i32.const 117)) ;; u
160 (i32.store8 (i32.const 2062) (i32.const 101)) ;; e
161 (i32.store8 (i32.const 2063) (i32.const 125)) ;; }
162
163 (global.get $result_ptr)
164 )
165 )
166 "#;
167
168 let wasm_bytes = wat::parse_str(wat)?;
169 use base64::{engine::general_purpose, Engine as _};
170 let b64 = general_purpose::STANDARD.encode(&wasm_bytes);
171 let module_path = format!("base64://{}", b64);
172
173 let executor = WasmExecutor::new();
174 let input = json!({"test": "data"});
175
176 let result = executor.execute(&module_path, "run", input).await?;
177
178 assert_eq!(result["echoed"], true);
179
180 Ok(())
181 }
182
183 #[tokio::test]
184 async fn test_wasm_execution_with_config() -> Result<()> {
185 let wat = r#"
186 (module
187 (memory (export "memory") 1)
188 (global $result_len (mut i32) (i32.const 0))
189 (global $result_ptr (mut i32) (i32.const 0))
190
191 (func (export "alloc") (param $len i32) (result i32)
192 (i32.const 1024)
193 )
194
195 (func (export "dealloc") (param $ptr i32) (param $len i32)
196 )
197
198 (func (export "get_result_len") (result i32)
199 (global.get $result_len)
200 )
201
202 (func (export "run") (param $ptr i32) (param $len i32) (result i32)
203 (global.set $result_ptr (i32.const 2048))
204 (global.set $result_len (i32.const 17))
205
206 ;; Write '{"config": "val"}' to 2048
207 (i32.store8 (i32.const 2048) (i32.const 123)) ;; {
208 (i32.store8 (i32.const 2049) (i32.const 34)) ;; "
209 (i32.store8 (i32.const 2050) (i32.const 99)) ;; c
210 (i32.store8 (i32.const 2051) (i32.const 111)) ;; o
211 (i32.store8 (i32.const 2052) (i32.const 110)) ;; n
212 (i32.store8 (i32.const 2053) (i32.const 102)) ;; f
213 (i32.store8 (i32.const 2054) (i32.const 105)) ;; i
214 (i32.store8 (i32.const 2055) (i32.const 103)) ;; g
215 (i32.store8 (i32.const 2056) (i32.const 34)) ;; "
216 (i32.store8 (i32.const 2057) (i32.const 58)) ;; :
217 (i32.store8 (i32.const 2058) (i32.const 32)) ;; space
218 (i32.store8 (i32.const 2059) (i32.const 34)) ;; "
219 (i32.store8 (i32.const 2060) (i32.const 118)) ;; v
220 (i32.store8 (i32.const 2061) (i32.const 97)) ;; a
221 (i32.store8 (i32.const 2062) (i32.const 108)) ;; l
222 (i32.store8 (i32.const 2063) (i32.const 34)) ;; "
223 (i32.store8 (i32.const 2064) (i32.const 125)) ;; }
224
225 (global.get $result_ptr)
226 )
227 )
228 "#;
229
230 let wasm_bytes = wat::parse_str(wat)?;
231 use base64::{engine::general_purpose, Engine as _};
232 let b64 = general_purpose::STANDARD.encode(&wasm_bytes);
233 let module_path = format!("base64://{}", b64);
234
235 let executor = WasmExecutor::new();
236 let input = json!({
237 "spec": {},
238 "params": {},
239 "inputs": {},
240 "config": {"key": "val"}
241 });
242
243 let result = executor.execute(&module_path, "run", input).await?;
244
245 assert_eq!(result["config"], "val");
246
247 Ok(())
248 }
249}