Skip to main content

stormchaser_engine/
wasm.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use wasmtime::*;
4
5/// Utility for executing WebAssembly (WASM) modules using Wasmtime.
6pub struct WasmExecutor {
7    engine: Engine,
8}
9
10impl Default for WasmExecutor {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl WasmExecutor {
17    /// Creates a new `WasmExecutor` with a default Wasmtime engine configuration.
18    pub fn new() -> Self {
19        let config = Config::new();
20        // config.async_support(true); // No longer needed in wasmtime 43.0
21        let engine = Engine::new(&config).expect("Failed to create Wasmtime engine");
22        Self { engine }
23    }
24
25    /// Execute.
26    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        // Add basic WASI if needed, but for now just pure WASM
55        // wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
56
57        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        // For simplicity, we assume the WASM module uses a shared memory and we can pass JSON.
63        // This requires a more complex interaction (allocating in WASM memory).
64        // For a prototype, let's assume a simpler interface if possible or just use string passing.
65
66        // If the function takes no args and returns nothing, it's easy.
67        // But we want to pass data.
68
69        // Let's implement a simple "JSON string in, JSON string out" via exported memory.
70
71        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        // Find allocation functions in WASM
79        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        // Assume the function returns a pointer to a result structure or a pointer to the result string directly.
92        // Let's assume it returns a pointer to a null-terminated string for now, or we can use another exported func to get length.
93
94        // For this implementation, let's assume it returns a pointer to the start of the result string,
95        // and we have a 'get_result_len' function.
96        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        // Cleanup
106        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        // A simple WAT that echoes the input back with "echoed": true
122        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}