Stream: general

Topic: How are lists allocated in WIT?


view this post on Zulip Sekoia (Mar 04 2025 at 17:11):

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).

view this post on Zulip Alex Crichton (Mar 04 2025 at 17:39):

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

view this post on Zulip Sekoia (Mar 04 2025 at 17:45):

Where is cabi_realloc documented, and how does it decide where it can write?

view this post on Zulip Alex Crichton (Mar 04 2025 at 17:46):

it's documented here, along with everything else about the canonical abi

Repository for design and specification of the Component Model - WebAssembly/component-model

view this post on Zulip Sekoia (Mar 04 2025 at 17:47):

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