use wasmtime::{Engine, Instance, Linker, Module, Store, Val};
use crate::error::{StatorError, StatorResult};
use crate::objects::value::JsValue;
#[derive(Clone, Debug)]
pub struct WasmEngine {
inner: Engine,
}
impl WasmEngine {
pub fn new() -> Self {
Self {
inner: Engine::default(),
}
}
pub fn inner(&self) -> &Engine {
&self.inner
}
}
impl Default for WasmEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct WasmModule {
inner: Module,
}
impl WasmModule {
pub fn from_bytes(engine: &WasmEngine, bytes: &[u8]) -> StatorResult<Self> {
Module::new(engine.inner(), bytes)
.map(|inner| Self { inner })
.map_err(|e| StatorError::WasmError(e.to_string()))
}
pub fn from_wat(engine: &WasmEngine, wat: &str) -> StatorResult<Self> {
Module::new(engine.inner(), wat)
.map(|inner| Self { inner })
.map_err(|e| StatorError::WasmError(e.to_string()))
}
pub fn inner(&self) -> &Module {
&self.inner
}
}
pub struct WasmInstance {
store: Store<()>,
inner: Instance,
}
impl WasmInstance {
pub fn new(engine: &WasmEngine, module: &WasmModule) -> StatorResult<Self> {
let mut store: Store<()> = Store::new(engine.inner(), ());
let linker: Linker<()> = Linker::new(engine.inner());
let instance = linker
.instantiate(&mut store, module.inner())
.map_err(|e| StatorError::WasmError(e.to_string()))?;
Ok(Self {
store,
inner: instance,
})
}
pub fn call(&mut self, name: &str, args: &[Val]) -> StatorResult<Vec<Val>> {
let func = self
.inner
.get_func(&mut self.store, name)
.ok_or_else(|| StatorError::WasmError(format!("no exported function '{name}'")))?;
let ty = func.ty(&self.store);
let result_count = ty.results().len();
let mut results = vec![Val::I32(0); result_count];
func.call(&mut self.store, args, &mut results)
.map_err(|e| StatorError::WasmError(e.to_string()))?;
Ok(results)
}
pub fn export_names(&mut self) -> Vec<String> {
self.inner
.exports(&mut self.store)
.map(|e| e.name().to_owned())
.collect()
}
pub fn call_with_js_values(&mut self, name: &str, args: &[JsValue]) -> StatorResult<JsValue> {
let wasm_args: Vec<Val> = args
.iter()
.map(js_value_to_wasm_val)
.collect::<StatorResult<Vec<_>>>()?;
let results = self.call(name, &wasm_args)?;
if results.is_empty() {
Ok(JsValue::Undefined)
} else {
wasm_val_to_js_value(&results[0])
}
}
pub fn inner(&self) -> &Instance {
&self.inner
}
}
pub fn js_value_to_wasm_val(value: &JsValue) -> StatorResult<Val> {
match value {
JsValue::Smi(n) => Ok(Val::I32(*n)),
JsValue::HeapNumber(f) => {
let as_i32 = *f as i32;
if f64::from(as_i32) == *f {
Ok(Val::I32(as_i32))
} else {
Ok(Val::F64(f.to_bits()))
}
}
JsValue::Boolean(b) => Ok(Val::I32(i32::from(*b))),
JsValue::Undefined | JsValue::Null => Ok(Val::I32(0)),
other => Err(StatorError::WasmError(format!(
"cannot convert JsValue::{:?} to a Wasm value",
std::mem::discriminant(other)
))),
}
}
pub fn wasm_val_to_js_value(val: &Val) -> StatorResult<JsValue> {
match val {
Val::I32(n) => Ok(JsValue::Smi(*n)),
Val::I64(n) => Ok(JsValue::HeapNumber(*n as f64)),
Val::F32(bits) => Ok(JsValue::HeapNumber(f64::from(f32::from_bits(*bits)))),
Val::F64(bits) => Ok(JsValue::HeapNumber(f64::from_bits(*bits))),
other => Err(StatorError::WasmError(format!(
"cannot convert Wasm value {:?} to a JsValue",
other
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wasm_engine_new() {
let engine = WasmEngine::new();
let _ = engine.inner();
}
#[test]
fn test_wasm_engine_default() {
let _engine: WasmEngine = WasmEngine::default();
}
#[test]
fn test_wasm_engine_clone() {
let engine = WasmEngine::new();
let _cloned = engine.clone();
}
const MINIMAL_WAT: &str = r#"(module)"#;
const ADD_WAT: &str = r#"
(module
(func $add (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
"#;
const ADD_F64_WAT: &str = r#"
(module
(func $addf (export "addf") (param f64 f64) (result f64)
local.get 0
local.get 1
f64.add))
"#;
const FACTORIAL_WAT: &str = r#"
(module
(func $factorial (export "factorial") (param i32) (result i32)
(if (result i32) (i32.le_s (local.get 0) (i32.const 1))
(then (i32.const 1))
(else
(i32.mul
(local.get 0)
(call $factorial (i32.sub (local.get 0) (i32.const 1)))))))
)
"#;
#[test]
fn test_wasm_module_from_wat_minimal() {
let engine = WasmEngine::new();
WasmModule::from_wat(&engine, MINIMAL_WAT).expect("minimal WAT should compile");
}
#[test]
fn test_wasm_module_from_wat_invalid() {
let engine = WasmEngine::new();
let err = WasmModule::from_wat(&engine, "not valid wat").unwrap_err();
assert!(matches!(err, StatorError::WasmError(_)));
}
#[test]
fn test_wasm_module_from_bytes_valid() {
let engine = WasmEngine::new();
let bytes: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
WasmModule::from_bytes(&engine, bytes).expect("empty wasm binary should compile");
}
#[test]
fn test_wasm_module_from_bytes_invalid() {
let engine = WasmEngine::new();
let err = WasmModule::from_bytes(&engine, b"not wasm").unwrap_err();
assert!(matches!(err, StatorError::WasmError(_)));
}
#[test]
fn test_wasm_module_clone() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, MINIMAL_WAT).unwrap();
let _cloned = module.clone();
}
#[test]
fn test_wasm_instance_new() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, MINIMAL_WAT).unwrap();
WasmInstance::new(&engine, &module)
.expect("instantiation of minimal module should succeed");
}
#[test]
fn test_wasm_instance_call_add() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, ADD_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let result = instance
.call("add", &[Val::I32(3), Val::I32(4)])
.expect("add(3, 4) should succeed");
assert_eq!(result.len(), 1);
assert_eq!(result[0].unwrap_i32(), 7);
}
#[test]
fn test_wasm_instance_call_add_f64() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, ADD_F64_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let a = 1.5_f64;
let b = 2.25_f64;
let result = instance
.call("addf", &[Val::F64(a.to_bits()), Val::F64(b.to_bits())])
.expect("addf(1.5, 2.25) should succeed");
assert_eq!(result.len(), 1);
assert_eq!(result[0].unwrap_f64(), 3.75);
}
#[test]
fn test_wasm_instance_call_factorial() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, FACTORIAL_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let result = instance
.call("factorial", &[Val::I32(5)])
.expect("factorial(5) should succeed");
assert_eq!(result[0].unwrap_i32(), 120);
}
#[test]
fn test_wasm_instance_call_missing_export() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, MINIMAL_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let err = instance.call("nonexistent", &[]).unwrap_err();
assert!(matches!(err, StatorError::WasmError(_)));
}
#[test]
fn test_js_value_to_wasm_val_smi() {
let v = js_value_to_wasm_val(&JsValue::Smi(42)).unwrap();
assert_eq!(v.unwrap_i32(), 42);
}
#[test]
fn test_js_value_to_wasm_val_heap_number_exact_i32() {
let v = js_value_to_wasm_val(&JsValue::HeapNumber(7.0)).unwrap();
assert_eq!(v.unwrap_i32(), 7);
}
#[test]
fn test_js_value_to_wasm_val_heap_number_f64() {
let v = js_value_to_wasm_val(&JsValue::HeapNumber(3.14)).unwrap();
assert_eq!(v.unwrap_f64(), 3.14);
}
#[test]
fn test_js_value_to_wasm_val_boolean_true() {
let v = js_value_to_wasm_val(&JsValue::Boolean(true)).unwrap();
assert_eq!(v.unwrap_i32(), 1);
}
#[test]
fn test_js_value_to_wasm_val_boolean_false() {
let v = js_value_to_wasm_val(&JsValue::Boolean(false)).unwrap();
assert_eq!(v.unwrap_i32(), 0);
}
#[test]
fn test_js_value_to_wasm_val_undefined() {
let v = js_value_to_wasm_val(&JsValue::Undefined).unwrap();
assert_eq!(v.unwrap_i32(), 0);
}
#[test]
fn test_js_value_to_wasm_val_null() {
let v = js_value_to_wasm_val(&JsValue::Null).unwrap();
assert_eq!(v.unwrap_i32(), 0);
}
#[test]
fn test_js_value_to_wasm_val_unsupported() {
let err = js_value_to_wasm_val(&JsValue::String("hello".into())).unwrap_err();
assert!(matches!(err, StatorError::WasmError(_)));
}
#[test]
fn test_wasm_val_to_js_value_i32() {
let v = wasm_val_to_js_value(&Val::I32(10)).unwrap();
assert_eq!(v, JsValue::Smi(10));
}
#[test]
fn test_wasm_val_to_js_value_i64() {
let v = wasm_val_to_js_value(&Val::I64(1_000_000_000_000_i64)).unwrap();
assert_eq!(v, JsValue::HeapNumber(1_000_000_000_000_f64));
}
#[test]
fn test_wasm_val_to_js_value_f32() {
let bits = 1.5_f32.to_bits();
let v = wasm_val_to_js_value(&Val::F32(bits)).unwrap();
if let JsValue::HeapNumber(f) = v {
assert!((f - 1.5).abs() < 1e-6);
} else {
panic!("expected HeapNumber");
}
}
#[test]
fn test_wasm_val_to_js_value_f64() {
let bits = 2.718_f64.to_bits();
let v = wasm_val_to_js_value(&Val::F64(bits)).unwrap();
assert_eq!(v, JsValue::HeapNumber(2.718));
}
#[test]
fn test_end_to_end_js_value_add() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, ADD_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let a = js_value_to_wasm_val(&JsValue::Smi(10)).unwrap();
let b = js_value_to_wasm_val(&JsValue::Smi(32)).unwrap();
let wasm_results = instance.call("add", &[a, b]).unwrap();
let js_result = wasm_val_to_js_value(&wasm_results[0]).unwrap();
assert_eq!(js_result, JsValue::Smi(42));
}
}