use crate::error::WasmError;
use crate::module::WasmModule;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use wasmtime::{Engine, Linker, Store, WasmParams, WasmResults};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};
pub struct WasmContext {
wasi: Option<WasiCtx>,
env: HashMap<String, String>,
cwd: Option<String>,
}
impl std::fmt::Debug for WasmContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WasmContext")
.field("wasi", &self.wasi.is_some())
.field("env", &self.env)
.field("cwd", &self.cwd)
.finish()
}
}
impl WasmContext {
pub fn new() -> Self {
Self {
wasi: None,
env: HashMap::new(),
cwd: None,
}
}
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = env;
self
}
pub fn with_cwd<S: Into<String>>(mut self, cwd: S) -> Self {
self.cwd = Some(cwd.into());
self
}
}
impl Default for WasmContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct WasmConfig {
pub max_memory: u64,
pub max_execution_time: Duration,
pub max_fuel: Option<u64>,
pub enable_wasi: bool,
pub allow_network: bool,
pub allow_filesystem: bool,
}
impl Default for WasmConfig {
fn default() -> Self {
Self {
max_memory: 64 * 1024 * 1024, max_execution_time: Duration::from_secs(30),
max_fuel: Some(1_000_000), enable_wasi: true,
allow_network: false,
allow_filesystem: false,
}
}
}
pub struct WasmRuntime {
engine: Engine,
config: WasmConfig,
}
impl WasmRuntime {
pub fn new() -> Result<Self, WasmError> {
Self::with_config(WasmConfig::default())
}
pub fn with_config(config: WasmConfig) -> Result<Self, WasmError> {
let mut wasmtime_config = wasmtime::Config::new();
wasmtime_config.max_wasm_stack(1024 * 1024);
if config.max_fuel.is_some() {
wasmtime_config.consume_fuel(true);
}
wasmtime_config.async_support(true);
let engine = Engine::new(&wasmtime_config)?;
Ok(WasmRuntime { engine, config })
}
pub async fn execute_json<T, R>(
&self,
module: &mut WasmModule,
input: &T,
context: WasmContext,
) -> Result<R, WasmError>
where
T: Serialize,
R: for<'de> Deserialize<'de>,
{
let input_json = serde_json::to_string(input)
.map_err(|e| WasmError::Execution(format!("Failed to serialize input: {}", e)))?;
let output_json = self.execute_with_stdio(module, &input_json, context).await?;
let output = serde_json::from_str(&output_json)
.map_err(|e| WasmError::Execution(format!("Failed to deserialize output: {}", e)))?;
Ok(output)
}
pub async fn execute_with_stdio(
&self,
module: &mut WasmModule,
input: &str,
context: WasmContext,
) -> Result<String, WasmError> {
let is_wasi = module.is_wasi();
let compiled_module = module.get_compiled(&self.engine)?;
let mut store = Store::new(&self.engine, context);
if let Some(fuel) = self.config.max_fuel {
store.add_fuel(fuel)?;
}
let mut linker = Linker::new(&self.engine);
if self.config.enable_wasi && is_wasi {
let mut wasi_builder = WasiCtxBuilder::new();
for (key, value) in &store.data().env {
let _ = wasi_builder.env(key, value);
}
let wasi_ctx = wasi_builder.build();
store.data_mut().wasi = Some(wasi_ctx);
wasmtime_wasi::add_to_linker(&mut linker, |ctx: &mut WasmContext| {
ctx.wasi.as_mut().unwrap()
})?;
let instance = linker.instantiate_async(&mut store, compiled_module).await?;
let start_func = instance
.get_typed_func::<(), ()>(&mut store, "_start")?;
let execution_future = start_func.call_async(&mut store, ());
let execution_result = tokio::time::timeout(
self.config.max_execution_time,
execution_future,
).await;
match execution_result {
Ok(Ok(())) => {
Ok(input.to_string())
}
Ok(Err(e)) => Err(WasmError::Execution(format!("WASM execution failed: {}", e))),
Err(_) => Err(WasmError::Execution("WASM execution timed out".to_string())),
}
} else {
let instance = linker.instantiate_async(&mut store, compiled_module).await?;
if let Ok(main_func) = instance.get_typed_func::<(), ()>(&mut store, "main") {
let execution_future = main_func.call_async(&mut store, ());
let execution_result = tokio::time::timeout(
self.config.max_execution_time,
execution_future,
).await;
match execution_result {
Ok(Ok(())) => Ok(String::new()), Ok(Err(e)) => Err(WasmError::Execution(format!("WASM execution failed: {}", e))),
Err(_) => Err(WasmError::Execution("WASM execution timed out".to_string())),
}
} else {
Err(WasmError::Execution(
"No suitable entry point found (main or _start)".to_string()
))
}
}
}
pub async fn call_function<Params, Results>(
&self,
module: &mut WasmModule,
function_name: &str,
params: Params,
context: WasmContext,
) -> Result<Results, WasmError>
where
Params: WasmParams,
Results: WasmResults,
{
let compiled_module = module.get_compiled(&self.engine)?;
let mut store = Store::new(&self.engine, context);
if let Some(fuel) = self.config.max_fuel {
store.add_fuel(fuel)?;
}
let linker = Linker::new(&self.engine);
let instance = linker.instantiate_async(&mut store, compiled_module).await?;
let func = instance.get_typed_func::<Params, Results>(&mut store, function_name)?;
let execution_future = func.call_async(&mut store, params);
let execution_result = tokio::time::timeout(
self.config.max_execution_time,
execution_future,
).await;
match execution_result {
Ok(Ok(result)) => Ok(result),
Ok(Err(e)) => Err(WasmError::Execution(format!("Function call failed: {}", e))),
Err(_) => Err(WasmError::Execution("Function call timed out".to_string())),
}
}
pub fn config(&self) -> &WasmConfig {
&self.config
}
}
impl Default for WasmRuntime {
fn default() -> Self {
Self::new().expect("Failed to create default WASM runtime")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::test_modules::{simple_function_wasm, wasi_hello_wasm};
use serde_json::json;
#[tokio::test]
async fn test_runtime_creation() {
let runtime = WasmRuntime::new().unwrap();
assert_eq!(runtime.config.max_memory, 64 * 1024 * 1024);
assert_eq!(runtime.config.max_execution_time, Duration::from_secs(30));
assert!(runtime.config.enable_wasi);
}
#[tokio::test]
async fn test_custom_config() {
let config = WasmConfig {
max_memory: 32 * 1024 * 1024,
max_execution_time: Duration::from_secs(10),
max_fuel: Some(500_000),
enable_wasi: false,
allow_network: false,
allow_filesystem: true,
};
let runtime = WasmRuntime::with_config(config).unwrap();
assert_eq!(runtime.config.max_memory, 32 * 1024 * 1024);
assert_eq!(runtime.config.max_execution_time, Duration::from_secs(10));
assert!(!runtime.config.enable_wasi);
assert!(runtime.config.allow_filesystem);
}
#[tokio::test]
async fn test_simple_function_call() {
let runtime = WasmRuntime::new().unwrap();
let mut module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
let context = WasmContext::new();
let result: i32 = runtime
.call_function(&mut module, "add", (5i32, 3i32), context)
.await
.unwrap();
assert_eq!(result, 8);
}
#[tokio::test]
async fn test_wasi_execution_basic() {
let runtime = WasmRuntime::new().unwrap();
let mut module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
let context = WasmContext::new();
let result = runtime
.execute_with_stdio(&mut module, "", context)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_json_serialization() {
let runtime = WasmRuntime::new().unwrap();
let mut module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
let context = WasmContext::new();
let input = json!({"message": "hello", "count": 42});
let result: Result<serde_json::Value, _> = runtime
.execute_json(&mut module, &input, context)
.await;
match result {
Ok(_) => {}, Err(WasmError::Execution(_)) => {}, Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[tokio::test]
async fn test_context_with_env() {
let mut env = HashMap::new();
env.insert("TEST_VAR".to_string(), "test_value".to_string());
let context = WasmContext::new()
.with_env(env)
.with_cwd("/tmp".to_string());
assert_eq!(context.env.get("TEST_VAR"), Some(&"test_value".to_string()));
assert_eq!(context.cwd, Some("/tmp".to_string()));
}
#[tokio::test]
async fn test_fuel_limit() {
let config = WasmConfig {
max_fuel: Some(100), ..Default::default()
};
let runtime = WasmRuntime::with_config(config).unwrap();
let mut module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
let context = WasmContext::new();
let result: Result<i32, _> = runtime
.call_function(&mut module, "add", (5i32, 3i32), context)
.await;
match result {
Ok(8) => {}, Ok(_) => panic!("Unexpected result value"),
Err(WasmError::Execution(_)) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
}
}
}