Hiya. I'm trying to reverse-engineering the generated bindings of WASIp2, and I noticed something strange:
In InputStream.read, the generated bindings are:
pub fn read(&self, len: u64) -> Result<_rt::Vec<u8>, StreamError> {
unsafe {
#[repr(align(4))]
struct RetArea([::core::mem::MaybeUninit<u8>; 12]);
let mut ret_area = RetArea(
[::core::mem::MaybeUninit::uninit(); 12],
);
let ptr0 = ret_area.0.as_mut_ptr().cast::<u8>();
#[cfg(target_arch = "wasm32")]
#[link(wasm_import_module = "wasi:io/streams@0.2.4")]
extern "C" {
#[link_name = "[method]input-stream.read"]
fn wit_import1(_: i32, _: i64, _: *mut u8);
}
#[cfg(not(target_arch = "wasm32"))]
extern "C" fn wit_import1(_: i32, _: i64, _: *mut u8) {
unreachable!()
}
wit_import1((self).handle() as i32, _rt::as_i64(&len), ptr0);
let l2 = i32::from(*ptr0.add(0).cast::<u8>());
let result9 = match l2 {
0 => {
let e = {
let l3 = *ptr0.add(4).cast::<*mut u8>();
let l4 = *ptr0.add(8).cast::<usize>();
let len5 = l4;
_rt::Vec::from_raw_parts(l3.cast(), len5, len5)
};
Ok(e)
}
1 => {
let e = {
let l6 = i32::from(*ptr0.add(4).cast::<u8>());
let v8 = match l6 {
0 => {
let e8 = {
let l7 = *ptr0.add(8).cast::<i32>();
super::super::super::wasi::io::error::Error::from_handle(
l7 as u32,
)
};
StreamError::LastOperationFailed(e8)
}
n => {
debug_assert_eq!(n, 1, "invalid enum discriminant");
StreamError::Closed
}
};
v8
};
Err(e)
}
_ => _rt::invalid_enum_discriminant(),
};
result9
}
}
Specifically: the location the stream writes to isn't passed as an argument, but it is given as a return value. That means that the input stream is somehow "allocating" memory, no? How is that safe? How does the host decide where to put the read data? (also, from _rt::as_i64(&len)
, it seems like the length is passed as a reference to a u64, which seems weird since it's the same size?)
I've tried to read the canonical ABI, but it's... not easy, and it's relatively subtle details here. It'd be great if there was some "debug view" of lowered functions/types... (idle thought, nothing else).
The len
there is "I'd like to read up to this amount" and the result is dynamically allocated into the guest with cabi_realloc
, and that return pointer is stored in the ptr0
return value area
Where is cabi_realloc
documented, and how does it decide where it can write?
it's documented here, along with everything else about the canonical abi
Oh, the c stands for canonical, not C. Makes sense (and it's documented as realloc
in there). So any WIT component that needs it must export a realloc
function, then? Interesting!
Last updated: Apr 08 2025 at 07:03 UTC