My guess is that you're not spotting the bug, because it's sensitive to something like timing, or output size, and by perturbing the format, you've changed the situation. Using JSON instead of bincode wrapped JSON will make this more obvious.
I think you were right about this. I removed bincode and the intermediary ResultTemp
struct, and just serialize to / from json and use from_slice
and to_vec
as you advised. Now I get the error again, and again only for some cases and again I can't reproduce it in pure rust - it only happens when I pass values back from WASM.
// to_vec() called on this value
Value(Object {"value": Array [Number(1), Number(2), Number(3)]})
// Expected value
// This is what I get when running tests in pure rust on the my DTO-crate and encoding / decoding works just fine
[7b, 22, 76, 61, 6c, 75, 65, 22, 3a, 7b, 22, 76, 61, 6c, 75, 65, 22, 3a, 5b, 31, 2c, 32, 2c, 33, 5d, 7d, 2c, 22, 65, 72, 72, 6f, 72, 22, 3a, 66, 61, 6c, 73, 65, 7d]
// These are the bytes returned from WASM
[74, f8, 11, 0, 74, f8, 11, 0, 3a, 7b, 22, 76, 61, 6c, 75, 65, 22, 3a, 5b, 31, 2c, 32, 2c, 33, 5d, 7d, 2c, 22, 65, 72, 72, 6f, 72, 22, 3a, 66, 61, 6c, 73, 65, 7d]
It now errors with this message:
thread 'runner::tests::it_runs' panicked at 'called `Result::unwrap()` on an `Err` value: expected ident at line 1 column 2', wa_run/src/runner.rs:151:39
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
These are the relevant parts of the code that compiles to WASM
// Host functions defined in "extern C" block
extern "C" {
fn argp() -> u32;
fn argl() -> u32;
fn resp(ptr: u32);
fn resl(len: u32);
}
#[no_mangle]
pub extern "C" fn run() {
let bytes = unsafe {
let arg_ptr = argp() as *mut u8;
let arg_len = argl();
Vec::from_raw_parts(arg_ptr, arg_len as usize, arg_len as usize)
};
let dto = CallDto::decode(bytes).unwrap();
let res = capture_error(dto, <CallDto as Call>::call)
.encode()
.unwrap();
let res_ptr = res.as_ptr() as u32;
let res_len = res.len() as u32;
unsafe {
resl(res_len);
resp(res_ptr);
};
}
And in rust using wasmtime it looks like below:
pub struct Runner {
engine: Engine,
module: Module,
linker: Linker<State>,
}
impl Runner {
pub fn new(file: &str) -> anyhow::Result<Self> {
let engine = Engine::default();
let module = Module::from_file(&engine, file)?;
let mut linker = Linker::new(&engine);
wasmtime_wasi::add_to_linker(&mut linker, |state: &mut State| &mut state.wasi)?;
let mut runner = Self {
engine,
module,
linker,
};
let argp = "argp";
runner
.linker
.func_wrap("env", argp, |mut caller: Caller<'_, State>| {
caller.data_mut().input.ptr
})?;
let argl = "argl";
runner
.linker
.func_wrap("env", argl, |mut caller: Caller<'_, State>| {
caller.data_mut().input.len
})?;
let resp = "resp";
runner
.linker
.func_wrap("env", resp, |mut caller: Caller<'_, State>, ptr: u32| {
caller.data_mut().output.ptr = ptr;
})?;
let resl = "resl";
runner
.linker
.func_wrap("env", resl, |mut caller: Caller<'_, State>, len: u32| {
caller.data_mut().output.len = len;
})?;
Ok(runner)
}
pub fn call(&self, dto: CallDto) -> anyhow::Result<ResultDto> {
let wasi = WasiCtxBuilder::new().build();
let data = State {
input: Mem { ptr: 0, len: 0 },
output: Mem { ptr: 0, len: 0 },
wasi,
};
let mut store = Store::new(&self.engine, data);
let instance = self.linker.instantiate(&mut store, &self.module)?;
let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
let memory = instance.get_memory(&mut store, "memory");
if let Some(m) = memory {
let ptr = 0;
let bytes = dto.encode()?;
let length = bytes.len() as u32;
store.data_mut().input.ptr = ptr;
store.data_mut().input.len = length;
m.write(&mut store, ptr as usize, &bytes)?;
}
run.call(&mut store, ())?;
let ptr = store.data_mut().output.ptr as usize;
let len = store.data_mut().output.len as usize;
let mut buf = vec![0; len];
if let Some(m) = memory {
m.read(&mut store, ptr, &mut buf)?;
}
drop(store);
let result = ResultDto::decode(buf.to_vec())?;
Ok(result)
}
}
For reference, this is their documentation on using linear memory from rust.
https://docs.wasmtime.dev/examples-rust-memory.html
As you can see, the functions resl()
and resp()
are defined in rust and called form the wasm module to set pointer and length value on the State
struct within the Wasmtime Store
. Values are then read to rust afterwards, and not within the scope of the argp()
.
An idea could be that since reading the bytes happens after argp()
scope has ended, then the memory I try to read from is not really owned by the current scope, and other processes may perhaps have written to it?
On the other hand, the WebAssembly linear memory is allocated by the Wasmtime function get_memory()
. To my understanding that piece of memory is still within scope and shouldn't be dropped until I call drop(store)
. There are not other calls within the Wasm module after argp()
, so I don't think there are any processes that should be able to write to that memory before I read it?