use std::sync::{Arc, Mutex};
use wasmtime::{
Config, Engine, FuncType, Instance, Linker, Module, Store, UpdateDeadline, Val, ValType,
};
use crate::error::{StatorError, StatorResult};
use crate::interpreter::{SCRIPT_TERMINATED_MESSAGE, check_interrupt_flag};
use crate::objects::value::JsValue;
static WASM_ENGINES: Mutex<Vec<Engine>> = Mutex::new(Vec::new());
pub fn interrupt_all_wasm_engines() {
let guard = WASM_ENGINES
.lock()
.expect("Stator Wasm engine registry mutex poisoned");
for engine in guard.iter() {
engine.increment_epoch();
}
}
fn register_engine(engine: &Engine) {
WASM_ENGINES
.lock()
.expect("Stator Wasm engine registry mutex poisoned")
.push(engine.clone());
}
#[derive(Clone, Debug)]
pub struct WasmEngine {
inner: Engine,
}
impl WasmEngine {
pub fn new() -> Self {
let mut config = Config::new();
config.epoch_interruption(true);
let inner = Engine::new(&config).expect("default wasmtime Config should be valid");
register_engine(&inner);
Self { inner }
}
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
}
}
#[repr(u32)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum HostValKind {
I32 = 0,
I64 = 1,
F32 = 2,
F64 = 3,
}
impl HostValKind {
fn to_wasmtime(self) -> ValType {
match self {
HostValKind::I32 => ValType::I32,
HostValKind::I64 => ValType::I64,
HostValKind::F32 => ValType::F32,
HostValKind::F64 => ValType::F64,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum HostVal {
I32(i32),
I64(i64),
F32(f32),
F64(f64),
}
impl HostVal {
pub fn kind(&self) -> HostValKind {
match self {
HostVal::I32(_) => HostValKind::I32,
HostVal::I64(_) => HostValKind::I64,
HostVal::F32(_) => HostValKind::F32,
HostVal::F64(_) => HostValKind::F64,
}
}
pub fn zero_of(kind: HostValKind) -> HostVal {
match kind {
HostValKind::I32 => HostVal::I32(0),
HostValKind::I64 => HostVal::I64(0),
HostValKind::F32 => HostVal::F32(0.0),
HostValKind::F64 => HostVal::F64(0.0),
}
}
fn from_wasm(v: &Val) -> Option<HostVal> {
match v {
Val::I32(n) => Some(HostVal::I32(*n)),
Val::I64(n) => Some(HostVal::I64(*n)),
Val::F32(b) => Some(HostVal::F32(f32::from_bits(*b))),
Val::F64(b) => Some(HostVal::F64(f64::from_bits(*b))),
_ => None,
}
}
fn to_wasm(self) -> Val {
match self {
HostVal::I32(n) => Val::I32(n),
HostVal::I64(n) => Val::I64(n),
HostVal::F32(f) => Val::F32(f.to_bits()),
HostVal::F64(f) => Val::F64(f.to_bits()),
}
}
}
pub type HostFuncCallback = Arc<dyn Fn(&[HostVal], &mut [HostVal]) -> bool + Send + Sync>;
pub struct HostFunc {
pub module: String,
pub name: String,
pub params: Vec<HostValKind>,
pub results: Vec<HostValKind>,
pub callback: HostFuncCallback,
}
pub struct WasmInstance {
store: Store<()>,
inner: Instance,
}
impl WasmInstance {
pub fn new(engine: &WasmEngine, module: &WasmModule) -> StatorResult<Self> {
Self::new_with_imports(engine, module, Vec::new())
}
pub fn new_with_imports(
engine: &WasmEngine,
module: &WasmModule,
imports: Vec<HostFunc>,
) -> StatorResult<Self> {
let mut store: Store<()> = Store::new(engine.inner(), ());
store.epoch_deadline_callback(|_| {
if check_interrupt_flag() {
return Ok(UpdateDeadline::Interrupt);
}
Ok(UpdateDeadline::Continue(1))
});
store.set_epoch_deadline(1);
let mut linker: Linker<()> = Linker::new(engine.inner());
for imp in imports {
let HostFunc {
module: m_name,
name: f_name,
params,
results,
callback,
} = imp;
let ty = FuncType::new(
engine.inner(),
params.iter().map(|k| k.to_wasmtime()),
results.iter().map(|k| k.to_wasmtime()),
);
let results_kinds = results.clone();
linker
.func_new(&m_name, &f_name, ty, move |_caller, args, out| {
if check_interrupt_flag() {
return Err(wasmtime::Error::msg(SCRIPT_TERMINATED_MESSAGE));
}
let host_args: Vec<HostVal> =
args.iter().filter_map(HostVal::from_wasm).collect();
if host_args.len() != args.len() {
return Err(wasmtime::Error::msg(
"unsupported Wasm value type at host import boundary",
));
}
let mut host_results: Vec<HostVal> =
results_kinds.iter().map(|k| HostVal::zero_of(*k)).collect();
let ok = (callback)(&host_args, &mut host_results);
if !ok {
if check_interrupt_flag() {
return Err(wasmtime::Error::msg(SCRIPT_TERMINATED_MESSAGE));
}
return Err(wasmtime::Error::msg("host function trapped"));
}
if check_interrupt_flag() {
return Err(wasmtime::Error::msg(SCRIPT_TERMINATED_MESSAGE));
}
for (slot, hv) in out.iter_mut().zip(host_results.iter()) {
*slot = hv.to_wasm();
}
Ok(())
})
.map_err(|e| StatorError::WasmError(e.to_string()))?;
}
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>> {
if check_interrupt_flag() {
return Err(StatorError::WasmError(
SCRIPT_TERMINATED_MESSAGE.to_string(),
));
}
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| {
if check_interrupt_flag() {
return StatorError::WasmError(SCRIPT_TERMINATED_MESSAGE.to_string());
}
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_wasm_call_entry_observes_interrupt_flag() {
use crate::interpreter::{clear_interrupt_flag, set_interrupt_flag};
use std::sync::atomic::{AtomicBool, Ordering};
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, ADD_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let ok = instance.call("add", &[Val::I32(1), Val::I32(2)]).unwrap();
assert_eq!(ok[0].unwrap_i32(), 3);
let flag = AtomicBool::new(true);
unsafe { set_interrupt_flag(&flag as *const _) };
let err = instance
.call("add", &[Val::I32(1), Val::I32(2)])
.expect_err("call must observe the interrupt flag at entry");
match err {
StatorError::WasmError(msg) => {
assert_eq!(SCRIPT_TERMINATED_MESSAGE, msg);
}
other => panic!("expected WasmError, got: {other:?}"),
}
flag.store(false, Ordering::Relaxed);
clear_interrupt_flag();
let ok = instance.call("add", &[Val::I32(10), Val::I32(20)]).unwrap();
assert_eq!(ok[0].unwrap_i32(), 30);
}
#[test]
fn test_interrupt_all_wasm_engines_safe_when_idle() {
let _engine = WasmEngine::new();
interrupt_all_wasm_engines();
interrupt_all_wasm_engines();
}
#[test]
fn test_wasm_epoch_bump_without_interrupt_flag_continues() {
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, ADD_WAT).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
engine.inner().increment_epoch();
let ok = instance.call("add", &[Val::I32(4), Val::I32(5)]).unwrap();
assert_eq!(ok[0].unwrap_i32(), 9);
}
#[test]
fn test_wasm_call_mid_execution_traps_when_interrupt_flag_set() {
use crate::interpreter::{clear_interrupt_flag, set_interrupt_flag};
use std::sync::atomic::AtomicBool;
let spin_wat = r#"
(module
(func (export "spin")
(loop $L (br $L))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, spin_wat).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let func = instance
.inner
.get_func(&mut instance.store, "spin")
.expect("spin export should exist");
let mut results = Vec::new();
let flag = AtomicBool::new(true);
engine.inner().increment_epoch();
unsafe { set_interrupt_flag(&flag as *const _) };
let err = func
.call(&mut instance.store, &[], &mut results)
.expect_err("spin must trap when the interrupt flag is set");
clear_interrupt_flag();
assert!(!err.to_string().is_empty());
}
#[test]
fn test_wasm_call_mid_execution_maps_interrupt_to_terminated_error() {
use crate::interpreter::{clear_interrupt_flag, set_interrupt_flag};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
let spin_wat = r#"
(module
(func (export "spin")
(loop $L (br $L))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, spin_wat).unwrap();
let mut instance = WasmInstance::new(&engine, &module).unwrap();
let flag = Arc::new(AtomicBool::new(false));
let trigger_flag = Arc::clone(&flag);
let trigger = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
trigger_flag.store(true, Ordering::Relaxed);
interrupt_all_wasm_engines();
});
unsafe { set_interrupt_flag(Arc::as_ptr(&flag)) };
let err = instance
.call("spin", &[])
.expect_err("spin must terminate after the async epoch bump");
clear_interrupt_flag();
trigger.join().unwrap();
match err {
StatorError::WasmError(msg) => {
assert_eq!(SCRIPT_TERMINATED_MESSAGE, msg);
}
other => panic!("expected WasmError, got: {other:?}"),
}
}
#[test]
fn test_wasm_host_import_add_i32() {
let wat = r#"
(module
(import "env" "add" (func $add (param i32 i32) (result i32)))
(func (export "call_add") (param i32 i32) (result i32)
local.get 0
local.get 1
call $add))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let cb: HostFuncCallback = Arc::new(|args, results| {
let a = match args[0] {
HostVal::I32(n) => n,
_ => return false,
};
let b = match args[1] {
HostVal::I32(n) => n,
_ => return false,
};
results[0] = HostVal::I32(a.wrapping_add(b));
true
});
let imports = vec![HostFunc {
module: "env".to_string(),
name: "add".to_string(),
params: vec![HostValKind::I32, HostValKind::I32],
results: vec![HostValKind::I32],
callback: cb,
}];
let mut instance = WasmInstance::new_with_imports(&engine, &module, imports).unwrap();
let r = instance
.call("call_add", &[Val::I32(7), Val::I32(35)])
.unwrap();
assert_eq!(r[0].unwrap_i32(), 42);
}
#[test]
fn test_wasm_host_import_callback_false_traps() {
let wat = r#"
(module
(import "env" "bad" (func $bad (result i32)))
(func (export "go") (result i32) (call $bad)))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let cb: HostFuncCallback = Arc::new(|_args, _results| false);
let imports = vec![HostFunc {
module: "env".to_string(),
name: "bad".to_string(),
params: vec![],
results: vec![HostValKind::I32],
callback: cb,
}];
let mut instance = WasmInstance::new_with_imports(&engine, &module, imports).unwrap();
let err = instance.call("go", &[]).unwrap_err();
assert!(matches!(err, StatorError::WasmError(_)));
}
#[test]
fn test_wasm_host_import_missing_fails_instantiate() {
let wat = r#"
(module
(import "env" "missing" (func (result i32))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let result = WasmInstance::new_with_imports(&engine, &module, vec![]);
match result {
Ok(_) => panic!("instantiation should fail when imports are missing"),
Err(StatorError::WasmError(_)) => {}
Err(other) => panic!("expected WasmError, got: {other:?}"),
}
}
#[test]
fn test_wasm_host_import_bad_name_fails_instantiate() {
let wat = r#"
(module
(import "env" "add" (func (param i32 i32) (result i32))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let cb: HostFuncCallback = Arc::new(|_args, _results| true);
let imports = vec![HostFunc {
module: "env".to_string(),
name: "other".to_string(),
params: vec![HostValKind::I32, HostValKind::I32],
results: vec![HostValKind::I32],
callback: cb,
}];
let result = WasmInstance::new_with_imports(&engine, &module, imports);
match result {
Ok(_) => panic!("instantiation should fail with mismatched import name"),
Err(StatorError::WasmError(_)) => {}
Err(other) => panic!("expected WasmError, got: {other:?}"),
}
}
#[test]
fn test_wasm_host_import_signature_mismatch_fails_instantiate() {
let wat = r#"
(module
(import "env" "f" (func (param i32 i32) (result i32))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let cb: HostFuncCallback = Arc::new(|_args, _results| true);
let imports = vec![HostFunc {
module: "env".to_string(),
name: "f".to_string(),
params: vec![HostValKind::F64, HostValKind::F64],
results: vec![HostValKind::F64],
callback: cb,
}];
let result = WasmInstance::new_with_imports(&engine, &module, imports);
match result {
Ok(_) => panic!("instantiation should fail with mismatched signature"),
Err(StatorError::WasmError(_)) => {}
Err(other) => panic!("expected WasmError, got: {other:?}"),
}
}
#[test]
fn test_wasm_host_import_termination_before_call_returns_terminated() {
use crate::interpreter::{clear_interrupt_flag, set_interrupt_flag};
use std::sync::atomic::AtomicBool;
let wat = r#"
(module
(import "env" "add" (func $add (param i32 i32) (result i32)))
(func (export "go") (result i32)
(call $add (i32.const 1) (i32.const 2))))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let cb: HostFuncCallback = Arc::new(|args, results| {
if let (HostVal::I32(a), HostVal::I32(b)) = (args[0], args[1]) {
results[0] = HostVal::I32(a + b);
true
} else {
false
}
});
let imports = vec![HostFunc {
module: "env".to_string(),
name: "add".to_string(),
params: vec![HostValKind::I32, HostValKind::I32],
results: vec![HostValKind::I32],
callback: cb,
}];
let mut instance = WasmInstance::new_with_imports(&engine, &module, imports).unwrap();
let flag = AtomicBool::new(true);
unsafe { set_interrupt_flag(&flag as *const _) };
let err = instance.call("go", &[]).unwrap_err();
clear_interrupt_flag();
match err {
StatorError::WasmError(msg) => assert_eq!(SCRIPT_TERMINATED_MESSAGE, msg),
other => panic!("expected WasmError, got: {other:?}"),
}
}
#[test]
fn test_wasm_host_import_runs_on_calling_thread() {
use std::sync::Mutex as StdMutex;
use std::thread::ThreadId;
let wat = r#"
(module
(import "env" "probe" (func $probe))
(func (export "go") (call $probe)))
"#;
let engine = WasmEngine::new();
let module = WasmModule::from_wat(&engine, wat).unwrap();
let observed: Arc<StdMutex<Option<ThreadId>>> = Arc::new(StdMutex::new(None));
let observed_cb = Arc::clone(&observed);
let cb: HostFuncCallback = Arc::new(move |_args, _results| {
*observed_cb.lock().unwrap() = Some(std::thread::current().id());
true
});
let imports = vec![HostFunc {
module: "env".to_string(),
name: "probe".to_string(),
params: vec![],
results: vec![],
callback: cb,
}];
let mut instance = WasmInstance::new_with_imports(&engine, &module, imports).unwrap();
instance.call("go", &[]).unwrap();
let seen = observed.lock().unwrap().expect("callback must have run");
assert_eq!(seen, std::thread::current().id());
}
#[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));
}
}