1use crate::error::WasmError;
4use crate::module::WasmModule;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::time::Duration;
8use wasmtime::{Engine, Linker, Store, WasmParams, WasmResults};
9use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};
10
11pub struct WasmContext {
13 wasi: Option<WasiCtx>,
15 env: HashMap<String, String>,
17 cwd: Option<String>,
19}
20
21impl std::fmt::Debug for WasmContext {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("WasmContext")
24 .field("wasi", &self.wasi.is_some())
25 .field("env", &self.env)
26 .field("cwd", &self.cwd)
27 .finish()
28 }
29}
30
31impl WasmContext {
32 pub fn new() -> Self {
34 Self {
35 wasi: None,
36 env: HashMap::new(),
37 cwd: None,
38 }
39 }
40
41 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
43 self.env = env;
44 self
45 }
46
47 pub fn with_cwd<S: Into<String>>(mut self, cwd: S) -> Self {
49 self.cwd = Some(cwd.into());
50 self
51 }
52}
53
54impl Default for WasmContext {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct WasmConfig {
63 pub max_memory: u64,
65 pub max_execution_time: Duration,
67 pub max_fuel: Option<u64>,
69 pub enable_wasi: bool,
71 pub allow_network: bool,
73 pub allow_filesystem: bool,
75}
76
77impl Default for WasmConfig {
78 fn default() -> Self {
79 Self {
80 max_memory: 64 * 1024 * 1024, max_execution_time: Duration::from_secs(30),
82 max_fuel: Some(1_000_000), enable_wasi: true,
84 allow_network: false,
85 allow_filesystem: false,
86 }
87 }
88}
89
90pub struct WasmRuntime {
92 engine: Engine,
94 config: WasmConfig,
96}
97
98impl WasmRuntime {
99 pub fn new() -> Result<Self, WasmError> {
101 Self::with_config(WasmConfig::default())
102 }
103
104 pub fn with_config(config: WasmConfig) -> Result<Self, WasmError> {
106 let mut wasmtime_config = wasmtime::Config::new();
107
108 wasmtime_config.max_wasm_stack(1024 * 1024); if config.max_fuel.is_some() {
113 wasmtime_config.consume_fuel(true);
114 }
115
116 wasmtime_config.async_support(true);
118
119 let engine = Engine::new(&wasmtime_config)?;
120
121 Ok(WasmRuntime { engine, config })
122 }
123
124 pub async fn execute_json<T, R>(
126 &self,
127 module: &mut WasmModule,
128 input: &T,
129 context: WasmContext,
130 ) -> Result<R, WasmError>
131 where
132 T: Serialize,
133 R: for<'de> Deserialize<'de>,
134 {
135 let input_json = serde_json::to_string(input)
137 .map_err(|e| WasmError::Execution(format!("Failed to serialize input: {}", e)))?;
138
139 let output_json = self.execute_with_stdio(module, &input_json, context).await?;
141
142 let output = serde_json::from_str(&output_json)
144 .map_err(|e| WasmError::Execution(format!("Failed to deserialize output: {}", e)))?;
145
146 Ok(output)
147 }
148
149 pub async fn execute_with_stdio(
151 &self,
152 module: &mut WasmModule,
153 input: &str,
154 context: WasmContext,
155 ) -> Result<String, WasmError> {
156 let is_wasi = module.is_wasi();
157 let compiled_module = module.get_compiled(&self.engine)?;
158
159 let mut store = Store::new(&self.engine, context);
161
162 if let Some(fuel) = self.config.max_fuel {
164 store.add_fuel(fuel)?;
165 }
166
167 let mut linker = Linker::new(&self.engine);
169
170 if self.config.enable_wasi && is_wasi {
171 let mut wasi_builder = WasiCtxBuilder::new();
173
174 for (key, value) in &store.data().env {
176 let _ = wasi_builder.env(key, value);
177 }
178
179 let wasi_ctx = wasi_builder.build();
181 store.data_mut().wasi = Some(wasi_ctx);
182
183 wasmtime_wasi::add_to_linker(&mut linker, |ctx: &mut WasmContext| {
185 ctx.wasi.as_mut().unwrap()
186 })?;
187
188 let instance = linker.instantiate_async(&mut store, compiled_module).await?;
190
191 let start_func = instance
193 .get_typed_func::<(), ()>(&mut store, "_start")?;
194
195 let execution_future = start_func.call_async(&mut store, ());
197 let execution_result = tokio::time::timeout(
198 self.config.max_execution_time,
199 execution_future,
200 ).await;
201
202 match execution_result {
203 Ok(Ok(())) => {
204 Ok(input.to_string())
207 }
208 Ok(Err(e)) => Err(WasmError::Execution(format!("WASM execution failed: {}", e))),
209 Err(_) => Err(WasmError::Execution("WASM execution timed out".to_string())),
210 }
211 } else {
212 let instance = linker.instantiate_async(&mut store, compiled_module).await?;
214
215 if let Ok(main_func) = instance.get_typed_func::<(), ()>(&mut store, "main") {
217 let execution_future = main_func.call_async(&mut store, ());
218 let execution_result = tokio::time::timeout(
219 self.config.max_execution_time,
220 execution_future,
221 ).await;
222
223 match execution_result {
224 Ok(Ok(())) => Ok(String::new()), Ok(Err(e)) => Err(WasmError::Execution(format!("WASM execution failed: {}", e))),
226 Err(_) => Err(WasmError::Execution("WASM execution timed out".to_string())),
227 }
228 } else {
229 Err(WasmError::Execution(
230 "No suitable entry point found (main or _start)".to_string()
231 ))
232 }
233 }
234 }
235
236 pub async fn call_function<Params, Results>(
238 &self,
239 module: &mut WasmModule,
240 function_name: &str,
241 params: Params,
242 context: WasmContext,
243 ) -> Result<Results, WasmError>
244 where
245 Params: WasmParams,
246 Results: WasmResults,
247 {
248 let compiled_module = module.get_compiled(&self.engine)?;
249 let mut store = Store::new(&self.engine, context);
250
251 if let Some(fuel) = self.config.max_fuel {
253 store.add_fuel(fuel)?;
254 }
255
256 let linker = Linker::new(&self.engine);
257 let instance = linker.instantiate_async(&mut store, compiled_module).await?;
258
259 let func = instance.get_typed_func::<Params, Results>(&mut store, function_name)?;
260
261 let execution_future = func.call_async(&mut store, params);
262 let execution_result = tokio::time::timeout(
263 self.config.max_execution_time,
264 execution_future,
265 ).await;
266
267 match execution_result {
268 Ok(Ok(result)) => Ok(result),
269 Ok(Err(e)) => Err(WasmError::Execution(format!("Function call failed: {}", e))),
270 Err(_) => Err(WasmError::Execution("Function call timed out".to_string())),
271 }
272 }
273
274 pub fn config(&self) -> &WasmConfig {
276 &self.config
277 }
278}
279
280impl Default for WasmRuntime {
281 fn default() -> Self {
282 Self::new().expect("Failed to create default WASM runtime")
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::test_utils::test_modules::{simple_function_wasm, wasi_hello_wasm};
290 use serde_json::json;
291
292 #[tokio::test]
293 async fn test_runtime_creation() {
294 let runtime = WasmRuntime::new().unwrap();
295 assert_eq!(runtime.config.max_memory, 64 * 1024 * 1024);
296 assert_eq!(runtime.config.max_execution_time, Duration::from_secs(30));
297 assert!(runtime.config.enable_wasi);
298 }
299
300 #[tokio::test]
301 async fn test_custom_config() {
302 let config = WasmConfig {
303 max_memory: 32 * 1024 * 1024,
304 max_execution_time: Duration::from_secs(10),
305 max_fuel: Some(500_000),
306 enable_wasi: false,
307 allow_network: false,
308 allow_filesystem: true,
309 };
310
311 let runtime = WasmRuntime::with_config(config).unwrap();
312 assert_eq!(runtime.config.max_memory, 32 * 1024 * 1024);
313 assert_eq!(runtime.config.max_execution_time, Duration::from_secs(10));
314 assert!(!runtime.config.enable_wasi);
315 assert!(runtime.config.allow_filesystem);
316 }
317
318 #[tokio::test]
319 async fn test_simple_function_call() {
320 let runtime = WasmRuntime::new().unwrap();
321 let mut module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
322 let context = WasmContext::new();
323
324 let result: i32 = runtime
326 .call_function(&mut module, "add", (5i32, 3i32), context)
327 .await
328 .unwrap();
329
330 assert_eq!(result, 8);
331 }
332
333 #[tokio::test]
334 async fn test_wasi_execution_basic() {
335 let runtime = WasmRuntime::new().unwrap();
336 let mut module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
337 let context = WasmContext::new();
338
339 let result = runtime
341 .execute_with_stdio(&mut module, "", context)
342 .await;
343
344 assert!(result.is_ok());
346 }
347
348 #[tokio::test]
349 async fn test_json_serialization() {
350 let runtime = WasmRuntime::new().unwrap();
351 let mut module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
352 let context = WasmContext::new();
353
354 let input = json!({"message": "hello", "count": 42});
355
356 let result: Result<serde_json::Value, _> = runtime
359 .execute_json(&mut module, &input, context)
360 .await;
361
362 match result {
365 Ok(_) => {}, Err(WasmError::Execution(_)) => {}, Err(e) => panic!("Unexpected error: {:?}", e),
368 }
369 }
370
371 #[tokio::test]
372 async fn test_context_with_env() {
373 let mut env = HashMap::new();
374 env.insert("TEST_VAR".to_string(), "test_value".to_string());
375
376 let context = WasmContext::new()
377 .with_env(env)
378 .with_cwd("/tmp".to_string());
379
380 assert_eq!(context.env.get("TEST_VAR"), Some(&"test_value".to_string()));
381 assert_eq!(context.cwd, Some("/tmp".to_string()));
382 }
383
384 #[tokio::test]
385 async fn test_fuel_limit() {
386 let config = WasmConfig {
387 max_fuel: Some(100), ..Default::default()
389 };
390
391 let runtime = WasmRuntime::with_config(config).unwrap();
392 let mut module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
393 let context = WasmContext::new();
394
395 let result: Result<i32, _> = runtime
397 .call_function(&mut module, "add", (5i32, 3i32), context)
398 .await;
399
400 match result {
402 Ok(8) => {}, Ok(_) => panic!("Unexpected result value"),
404 Err(WasmError::Execution(_)) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
406 }
407 }
408}