use wasm3x::{
Caller, CompilationMode, Config, Engine, Error, FuncType, Linker, Module, Mutability, Store,
Val, ValType,
};
fn wasm(wat: &str) -> Vec<u8> {
wat::parse_str(wat).expect("valid wat")
}
fn instantiate(wat: &str) -> (Store<()>, wasm3x::Instance) {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, wasm(wat)).expect("module parses");
let linker = Linker::<()>::new(&engine);
let instance = linker
.instantiate_and_start(&mut store, &module)
.expect("instantiation succeeds");
(store, instance)
}
#[test]
fn typed_add() {
let (mut store, instance) = instantiate(
r#"(module (func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))"#,
);
let add = instance
.get_typed_func::<(i32, i32), i32>(&store, "add")
.unwrap();
assert_eq!(add.call(&mut store, (2, 3)).unwrap(), 5);
assert_eq!(add.call(&mut store, (-10, 4)).unwrap(), -6);
}
#[test]
fn typed_recursive_fib() {
let (mut store, instance) = instantiate(
r#"(module (func $fib (export "fib") (param i32) (result i32)
(if (result i32) (i32.lt_s (local.get 0) (i32.const 2))
(then (local.get 0))
(else (i32.add (call $fib (i32.sub (local.get 0) (i32.const 1)))
(call $fib (i32.sub (local.get 0) (i32.const 2))))))))"#,
);
let fib = instance.get_typed_func::<i32, i32>(&store, "fib").unwrap();
assert_eq!(fib.call(&mut store, 10).unwrap(), 55);
assert_eq!(fib.call(&mut store, 20).unwrap(), 6765);
}
#[test]
fn bench_shaped_run_i64() {
let (mut store, instance) = instantiate(
r#"(module (func (export "run") (param i64) (result i64)
local.get 0 i64.const 1 i64.add))"#,
);
let run = instance.get_typed_func::<i64, i64>(&store, "run").unwrap();
assert_eq!(run.call(&mut store, 41).unwrap(), 42);
}
#[test]
fn dynamic_call_with_vals() {
let (mut store, instance) = instantiate(
r#"(module (func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))"#,
);
let func = instance.get_func(&store, "add").unwrap();
let ty = func.ty(&store).unwrap();
assert_eq!(ty.params(), &[ValType::I32, ValType::I32]);
assert_eq!(ty.results(), &[ValType::I32]);
let params = [Val::I32(20), Val::I32(22)];
let mut results = [Val::default_for_ty(ty.results()[0])];
func.call(&mut store, ¶ms, &mut results).unwrap();
assert_eq!(results[0], Val::I32(42));
}
#[test]
fn dynamic_multi_value_results() {
let (mut store, instance) = instantiate(
r#"(module (func (export "swap") (param i32 i64) (result i64 i32)
local.get 1 local.get 0))"#,
);
let func = instance.get_func(&store, "swap").unwrap();
assert_eq!(
func.ty(&store).unwrap().results(),
&[ValType::I64, ValType::I32]
);
let mut results = [Val::I64(0), Val::I32(0)];
func.call(&mut store, &[Val::I32(7), Val::I64(9)], &mut results)
.unwrap();
assert_eq!(results, [Val::I64(9), Val::I32(7)]);
}
#[test]
fn float_roundtrip() {
let (mut store, instance) = instantiate(
r#"(module (func (export "mul") (param f64 f64) (result f64)
local.get 0 local.get 1 f64.mul))"#,
);
let mul = instance
.get_typed_func::<(f64, f64), f64>(&store, "mul")
.unwrap();
assert_eq!(mul.call(&mut store, (1.5, 4.0)).unwrap(), 6.0);
}
#[test]
fn memory_read_write() {
let (mut store, instance) = instantiate(
r#"(module
(memory (export "mem") 1)
(func (export "store") (param i32 i32) local.get 0 local.get 1 i32.store)
(func (export "load") (param i32) (result i32) local.get 0 i32.load))"#,
);
let store_fn = instance
.get_typed_func::<(i32, i32), ()>(&store, "store")
.unwrap();
let load_fn = instance.get_typed_func::<i32, i32>(&store, "load").unwrap();
store_fn
.call(&mut store, (0, 0xDEAD_BEEFu32 as i32))
.unwrap();
assert_eq!(load_fn.call(&mut store, 0).unwrap(), 0xDEAD_BEEFu32 as i32);
let memory = instance.get_memory(&store).unwrap();
let mut buf = [0u8; 4];
memory.read(&store, 0, &mut buf).unwrap();
assert_eq!(u32::from_le_bytes(buf), 0xDEAD_BEEF);
memory.write(&mut store, 4, &[1, 2, 3, 4]).unwrap();
assert_eq!(
load_fn.call(&mut store, 4).unwrap(),
i32::from_le_bytes([1, 2, 3, 4])
);
assert!(memory.data_size(&store) >= 65536);
assert_eq!(memory.size(&store), 1);
}
#[test]
fn global_get_set() {
let (mut store, instance) = instantiate(
r#"(module
(global (export "g") (mut i32) (i32.const 7))
(global (export "c") i32 (i32.const 5))
(func (export "get_g") (result i32) global.get 0))"#,
);
let global = instance.get_global(&store, "g").unwrap();
let gt = global.ty(&store).unwrap();
assert_eq!(gt.content(), ValType::I32);
assert_eq!(gt.mutability(), Mutability::Mutable);
assert_eq!(global.get(&store).unwrap(), Val::I32(7));
global.set(&mut store, Val::I32(99)).unwrap();
let get_g = instance.get_typed_func::<(), i32>(&store, "get_g").unwrap();
assert_eq!(get_g.call(&mut store, ()).unwrap(), 99);
let konst = instance.get_global(&store, "c").unwrap();
let ct = konst.ty(&store).unwrap();
assert_eq!(ct.content(), ValType::I32);
assert_eq!(ct.mutability(), Mutability::Const);
assert!(konst.set(&mut store, Val::I32(1)).is_err());
}
#[test]
fn host_function_stateless() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "add1" (func $add1 (param i32) (result i32)))
(func (export "call_add1") (param i32) (result i32)
local.get 0 call $add1))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_wrap("env", "add1", |_caller: Caller<'_, ()>, x: i32| x + 1)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let call = instance
.get_typed_func::<i32, i32>(&store, "call_add1")
.unwrap();
assert_eq!(call.call(&mut store, 41).unwrap(), 42);
}
#[test]
fn host_function_trap_propagates() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "boom" (func $boom (param i32) (result i32)))
(func (export "go") (param i32) (result i32)
local.get 0 call $boom))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_wrap(
"env",
"boom",
|_caller: Caller<'_, ()>, _x: i32| -> Result<i32, Error> { Err(Error::host("kaboom")) },
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance.get_typed_func::<i32, i32>(&store, "go").unwrap();
let err = go.call(&mut store, 1).unwrap_err();
assert!(err.is_host(), "expected host error, got: {err}");
assert!(err.to_string().contains("kaboom"), "got: {err}");
}
#[test]
fn func_new_dynamic_host_function() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "sum" (func $sum (param i32 i32) (result i32)))
(func (export "go") (param i32 i32) (result i32)
local.get 0 local.get 1 call $sum))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_new(
"env",
"sum",
FuncType::new([ValType::I32, ValType::I32], [ValType::I32]),
|_caller: Caller<'_, ()>, args: &[Val], results: &mut [Val]| {
let a = args[0].i32().unwrap();
let b = args[1].i32().unwrap();
results[0] = Val::I32(a + b);
Ok(())
},
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance
.get_typed_func::<(i32, i32), i32>(&store, "go")
.unwrap();
assert_eq!(go.call(&mut store, (40, 2)).unwrap(), 42);
}
#[test]
fn func_new_large_signature_uses_heap_fallback() {
const N: usize = 20;
let params = "i32 ".repeat(N);
let consts: String = (1..=N).map(|i| format!("i32.const {i} ")).collect();
let wat = format!(
r#"(module
(import "env" "sum" (func $sum (param {params}) (result i32)))
(func (export "go") (result i32)
{consts} call $sum))"#
);
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, wasm(&wat)).unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_new(
"env",
"sum",
FuncType::new([ValType::I32; N], [ValType::I32]),
|_caller: Caller<'_, ()>, args: &[Val], results: &mut [Val]| {
let sum: i32 = args.iter().map(|v| v.i32().unwrap()).sum();
results[0] = Val::I32(sum);
Ok(())
},
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance.get_typed_func::<(), i32>(&store, "go").unwrap();
let expected: i32 = (1..=N as i32).sum();
assert_eq!(go.call(&mut store, ()).unwrap(), expected);
}
#[test]
fn func_new_zero_args_zero_results() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "noop" (func $noop))
(func (export "go") call $noop))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_new(
"env",
"noop",
FuncType::new([], []),
|_caller: Caller<'_, ()>, args: &[Val], results: &mut [Val]| {
assert!(args.is_empty());
assert!(results.is_empty());
Ok(())
},
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance.get_typed_func::<(), ()>(&store, "go").unwrap();
go.call(&mut store, ()).unwrap();
}
#[test]
fn func_wrap_multi_arg_mixed_types_round_trip() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "combine"
(func $combine (param i32 i64 f32 f64) (result f64)))
(func (export "go") (param i32 i64 f32 f64) (result f64)
local.get 0 local.get 1 local.get 2 local.get 3 call $combine))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_wrap(
"env",
"combine",
|_c: Caller<'_, ()>, a: i32, b: i64, c: f32, d: f64| -> f64 {
a as f64 + b as f64 + c as f64 + d
},
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance
.get_typed_func::<(i32, i64, f32, f64), f64>(&store, "go")
.unwrap();
let got = go.call(&mut store, (1, 20, 3.0, 400.0)).unwrap();
assert_eq!(got, 424.0);
}
#[test]
fn func_wrap_zero_arg_zero_result() {
let engine = Engine::default();
let mut store = Store::new(&engine, 0u32);
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "ping" (func $ping))
(func (export "go") call $ping))"#,
),
)
.unwrap();
let mut linker = Linker::<u32>::new(&engine);
linker
.func_wrap("env", "ping", |mut c: Caller<'_, u32>| {
*c.data_mut() += 1;
})
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance.get_typed_func::<(), ()>(&store, "go").unwrap();
go.call(&mut store, ()).unwrap();
assert_eq!(*store.data(), 1);
}
#[test]
fn eager_compilation_succeeds() {
let mut config = Config::new();
config.compilation_mode(CompilationMode::Eager);
let engine = Engine::new(&config);
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(r#"(module (func (export "answer") (result i32) i32.const 42))"#),
)
.unwrap();
let linker = Linker::<()>::new(&engine);
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let answer = instance
.get_typed_func::<(), i32>(&store, "answer")
.unwrap();
assert_eq!(answer.call(&mut store, ()).unwrap(), 42);
}
#[test]
fn eager_surfaces_missing_import_at_instantiation() {
let mut config = Config::new();
config.compilation_mode(CompilationMode::Eager);
let engine = Engine::new(&config);
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "missing" (func (result i32)))
(func (export "go") (result i32) call 0))"#,
),
)
.unwrap();
let linker = Linker::<()>::new(&engine);
assert!(linker.instantiate_and_start(&mut store, &module).is_err());
}
#[test]
fn module_instantiated_into_multiple_stores() {
let engine = Engine::default();
let module = Module::new(
&engine,
wasm(r#"(module (func (export "id") (param i32) (result i32) local.get 0))"#),
)
.unwrap();
let linker = Linker::<()>::new(&engine);
for n in 0..4 {
let mut store = Store::new(&engine, ());
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let id = instance.get_typed_func::<i32, i32>(&store, "id").unwrap();
assert_eq!(id.call(&mut store, n).unwrap(), n);
}
}
#[test]
fn typed_signature_mismatch_is_rejected() {
let (store, instance) = instantiate(
r#"(module (func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))"#,
);
assert!(
instance
.get_typed_func::<(i32, i32), i64>(&store, "add")
.is_err()
);
assert!(instance.get_typed_func::<i32, i32>(&store, "add").is_err());
}
#[test]
fn host_function_mutates_store_data() {
let engine = Engine::default();
let mut store = Store::new(&engine, 0u32);
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "tick" (func $tick (param i32) (result i32)))
(func (export "go") (param i32) (result i32)
local.get 0 call $tick))"#,
),
)
.unwrap();
let mut linker = Linker::<u32>::new(&engine);
linker
.func_wrap("env", "tick", |mut caller: Caller<'_, u32>, x: i32| {
*caller.data_mut() += 1;
x + *caller.data() as i32
})
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let go = instance.get_typed_func::<i32, i32>(&store, "go").unwrap();
assert_eq!(go.call(&mut store, 10).unwrap(), 11); assert_eq!(go.call(&mut store, 10).unwrap(), 12); assert_eq!(*store.data(), 2);
}
#[test]
fn host_function_reads_wasm_memory() {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(
&engine,
wasm(
r#"(module
(import "env" "sum" (func $sum (param i32 i32) (result i32)))
(memory (export "mem") 1)
(func (export "run") (param i32) (result i32)
;; write bytes [1,2,3,4] at offset 0, then hand (ptr,len) to host
(i32.store8 (i32.const 0) (i32.const 1))
(i32.store8 (i32.const 1) (i32.const 2))
(i32.store8 (i32.const 2) (i32.const 3))
(i32.store8 (i32.const 3) (i32.const 4))
(call $sum (i32.const 0) (i32.const 4))))"#,
),
)
.unwrap();
let mut linker = Linker::<()>::new(&engine);
linker
.func_wrap(
"env",
"sum",
|caller: Caller<'_, ()>, ptr: i32, len: i32| -> Result<i32, Error> {
let memory = caller
.get_memory()
.ok_or_else(|| Error::host("no memory"))?;
let mut buf = vec![0u8; len as usize];
memory.read(&caller, ptr as usize, &mut buf)?;
Ok(buf.iter().map(|&b| b as i32).sum())
},
)
.unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let run = instance.get_typed_func::<i32, i32>(&store, "run").unwrap();
assert_eq!(run.call(&mut store, 0).unwrap(), 1 + 2 + 3 + 4);
}
#[test]
fn memory_write_through_context_traits() {
let (mut store, instance) = instantiate(
r#"(module
(memory (export "mem") 1)
(func (export "load") (param i32) (result i32) local.get 0 i32.load))"#,
);
let memory = instance.get_memory(&store).unwrap();
let load = instance.get_typed_func::<i32, i32>(&store, "load").unwrap();
memory.write(&mut store, 0, &42u32.to_le_bytes()).unwrap();
assert_eq!(load.call(&mut store, 0).unwrap(), 42);
let mut buf = [0u8; 4];
memory.read(&store, 0, &mut buf).unwrap();
assert_eq!(u32::from_le_bytes(buf), 42);
}
#[test]
fn dropping_stores_is_sound() {
let engine = Engine::default();
let module = Module::new(
&engine,
wasm(r#"(module (func (export "answer") (result i32) i32.const 42))"#),
)
.unwrap();
for _ in 0..50 {
let mut store = Store::new(&engine, ());
let mut linker = Linker::<()>::new(&engine);
linker
.func_wrap("env", "unused", |_caller: Caller<'_, ()>| 0i32)
.unwrap();
let _instance = linker.instantiate_and_start(&mut store, &module).unwrap();
drop(store);
}
}