mitoxide_wasm/
runtime.rs

1//! WASM execution runtime
2
3use 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
11/// WASM execution context with WASI support
12pub struct WasmContext {
13    /// WASI context for the WASM module
14    wasi: Option<WasiCtx>,
15    /// Environment variables
16    env: HashMap<String, String>,
17    /// Working directory
18    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    /// Create a new WASM context
33    pub fn new() -> Self {
34        Self {
35            wasi: None,
36            env: HashMap::new(),
37            cwd: None,
38        }
39    }
40    
41    /// Set environment variables
42    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
43        self.env = env;
44        self
45    }
46    
47    /// Set working directory
48    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/// Configuration for WASM execution
61#[derive(Debug, Clone)]
62pub struct WasmConfig {
63    /// Maximum memory size in bytes (default: 64MB)
64    pub max_memory: u64,
65    /// Maximum execution time (default: 30 seconds)
66    pub max_execution_time: Duration,
67    /// Maximum fuel (instruction count limit)
68    pub max_fuel: Option<u64>,
69    /// Enable WASI support
70    pub enable_wasi: bool,
71    /// Allow network access (if WASI networking is supported)
72    pub allow_network: bool,
73    /// Allow filesystem access
74    pub allow_filesystem: bool,
75}
76
77impl Default for WasmConfig {
78    fn default() -> Self {
79        Self {
80            max_memory: 64 * 1024 * 1024, // 64MB
81            max_execution_time: Duration::from_secs(30),
82            max_fuel: Some(1_000_000), // 1M instructions
83            enable_wasi: true,
84            allow_network: false,
85            allow_filesystem: false,
86        }
87    }
88}
89
90/// WASM execution runtime with wasmtime integration
91pub struct WasmRuntime {
92    /// Wasmtime engine
93    engine: Engine,
94    /// Runtime configuration
95    config: WasmConfig,
96}
97
98impl WasmRuntime {
99    /// Create a new WASM runtime with default configuration
100    pub fn new() -> Result<Self, WasmError> {
101        Self::with_config(WasmConfig::default())
102    }
103    
104    /// Create a new WASM runtime with custom configuration
105    pub fn with_config(config: WasmConfig) -> Result<Self, WasmError> {
106        let mut wasmtime_config = wasmtime::Config::new();
107        
108        // Configure memory limits
109        wasmtime_config.max_wasm_stack(1024 * 1024); // 1MB stack
110        
111        // Configure fuel (instruction counting) if enabled
112        if config.max_fuel.is_some() {
113            wasmtime_config.consume_fuel(true);
114        }
115        
116        // Enable async support for timeouts
117        wasmtime_config.async_support(true);
118        
119        let engine = Engine::new(&wasmtime_config)?;
120        
121        Ok(WasmRuntime { engine, config })
122    }
123    
124    /// Execute a WASM module with JSON input/output
125    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        // Serialize input to JSON
136        let input_json = serde_json::to_string(input)
137            .map_err(|e| WasmError::Execution(format!("Failed to serialize input: {}", e)))?;
138        
139        // Execute with JSON string
140        let output_json = self.execute_with_stdio(module, &input_json, context).await?;
141        
142        // Deserialize output from JSON
143        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    /// Execute a WASM module with string input/output via stdio
150    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        // Create store with context
160        let mut store = Store::new(&self.engine, context);
161        
162        // Set fuel limit if configured
163        if let Some(fuel) = self.config.max_fuel {
164            store.add_fuel(fuel)?;
165        }
166        
167        // Create linker and add WASI if needed
168        let mut linker = Linker::new(&self.engine);
169        
170        if self.config.enable_wasi && is_wasi {
171            // Configure WASI context with basic setup
172            let mut wasi_builder = WasiCtxBuilder::new();
173            
174            // Add environment variables
175            for (key, value) in &store.data().env {
176                let _ = wasi_builder.env(key, value);
177            }
178            
179            // Build WASI context
180            let wasi_ctx = wasi_builder.build();
181            store.data_mut().wasi = Some(wasi_ctx);
182            
183            // Add WASI to linker
184            wasmtime_wasi::add_to_linker(&mut linker, |ctx: &mut WasmContext| {
185                ctx.wasi.as_mut().unwrap()
186            })?;
187            
188            // Instantiate the module
189            let instance = linker.instantiate_async(&mut store, compiled_module).await?;
190            
191            // Get the _start function for WASI modules
192            let start_func = instance
193                .get_typed_func::<(), ()>(&mut store, "_start")?;
194            
195            // Execute with timeout
196            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                    // Return the input as output for now (echo behavior)
205                    // In a real implementation, we'd capture actual stdout
206                    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            // Non-WASI execution - look for a main function or exported function
213            let instance = linker.instantiate_async(&mut store, compiled_module).await?;
214            
215            // Try to find a suitable entry point
216            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()), // No output for non-WASI
225                    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    /// Execute a WASM function directly with typed parameters
237    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        // Set fuel limit if configured
252        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    /// Get the runtime configuration
275    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        // Call the add function with parameters (5, 3)
325        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        // Execute WASI module (it should run without error even if it doesn't produce output)
340        let result = runtime
341            .execute_with_stdio(&mut module, "", context)
342            .await;
343        
344        // The module should execute successfully
345        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        // This will fail because our test WASI module doesn't actually process JSON,
357        // but it tests the serialization path
358        let result: Result<serde_json::Value, _> = runtime
359            .execute_json(&mut module, &input, context)
360            .await;
361        
362        // The execution should complete (though the JSON parsing might fail)
363        // This tests that the serialization/execution pipeline works
364        match result {
365            Ok(_) => {}, // Success case
366            Err(WasmError::Execution(_)) => {}, // Expected for our simple test module
367            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), // Very low fuel limit
388            ..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        // This might fail due to fuel exhaustion, which is expected behavior
396        let result: Result<i32, _> = runtime
397            .call_function(&mut module, "add", (5i32, 3i32), context)
398            .await;
399        
400        // Either succeeds (if the function is simple enough) or fails with fuel exhaustion
401        match result {
402            Ok(8) => {}, // Function completed within fuel limit
403            Ok(_) => panic!("Unexpected result value"),
404            Err(WasmError::Execution(_)) => {}, // Fuel exhausted or other execution error
405            Err(e) => panic!("Unexpected error type: {:?}", e),
406        }
407    }
408}