@Dan Gohman I think this is going to be a pretty meaty implementation so I figured I'd start a stream here and see where things go.
One wrinkle I just found (irrespective of *.wat
vs *.rs
or w/e) -- how is readlink
going to work? For example the preview1 API takes a buffer to fill in, but in the canonical ABI it would look more like path_readlink: func(fd: fd) -> result<string, errno>
. The difference here is that the cabi_realloc
call to allocate space for the string will not necessarily fit within the buffer provided, meaning that if the buffer is too small I'm not sure what the shim module can do because it's too late to learn that and there's nowhere else to return the string.
one option would be to fail the allocation in the cabi_realloc
implementation, but that would trap and destroy the module which sounds bad
the only other option I can think of though is to fit the return value on the stack page and then return E2BIG
or w/e to the original caller, but there's no guarantee that the value can fit on the stack page, which further might lead to a trap
oh you know the best solution might be something like path_readlink_compat: func(fd: fd, max: u32) -> result<string, errno>
where the host simply returns E2BIG
if the size is larger than max
For readlink
, users will typically either use a buffer of size libc::PATH_MAX
, which is 4096 in wasi-libc so it'll almost always be big enough, or they'll use a smaller buffer and be prepared for a ENAMETOOLONG
error
Or oh, readlink
's way of indicating that the name is truncated is to return a number of bytes which is equal to the provided buffer size.
this is part of the adapter though, where if the new function is readlink: func(fd) -> string
then the return value is communicated by calling malloc which the embedder assumes has the right size
this means that we can't naively "polyfill" preview1 readlink with something that is -> string
because the buffer provided to preview1's readlink originally may not be large enough
Ok, my first cut at generating a wat for this is here: https://github.com/sunfishcode/preview1.wasm/blob/main/pregenerated/preview1.wat
the code is here: https://github.com/sunfishcode/preview1.wasm/blob/main
As we discussed, it needs some fixing up, but it shows the idea here. I've implemented random_get
, where the wit API returns list<u8>
, so far.
hm this is using dlmalloc
though which I think we want to remove
Yeah, though it's convenient for now, because it's a real malloc.
I added an implementation of path_readlink to preview1.c, to show how it works
this works with a full-blown malloc
yeah but I think we want to avoid that
__wasm_init_memory
also looks problematic
but that's probably just there b/c of dlmalloc
weird, that __wasm_init_memory is just doing a memory.fill to zero out memory which should already be zeroed
the fact that it's present though is worrisome, because it may mean .rodata
/.data
is in play which we can't have
(which is one of the reasons I initially didn't want to use a high level language)
Ah, it does use static memory, for things like RET_AREA
at least
oh, dlmalloc has some static state too
this is what I meant by writing this would almost be an entirely different dialect of C
and there's no static assurances that you're not "doing the wrong thing"
and when the wrong thing is done it's not obvious always what did it
If I stub out malloc and hack RET_AREA
, there's no __wasm_init_memory
agreed yeah
but you will need access to custom wasm globals
to get the pointers provided to preview1 functions into the cabi_realloc function
yeah, there should be a way to do that, let me check
IIRC inline asm should give you a way to shove it in
To be sure, I agree this thing is in a special dialect of C and it's tricky. But it might still be better than writing all the bindings in raw wat
I've now added code to create wasm globals, and added a minimal malloc implementation that uses them.
And https://github.com/bytecodealliance/wit-bindgen/pull/326 is a PR to move RET_AREA out of static memory.
With those changes, we no longer have a __wasm_init_memory.
looking at the malloc
changes here again, what I was originally imagining is that when you called fd_read
, for example, the malloc
return value would be the pointer passed to fd_read
(similarly for path_readlink
or things like that). That way we don't have to worry about free
and things like that and we're basically leveraging the fact that we know, statically, the order that cabi_realloc
will be called in and with what sizes
also, if you're up for it, I'd personally prefer if we could get this in Rust instead of C since that'll likely be much easier to maintain over time
the final thing I think is that we'll need to figure out the update to the $__stack_pointer
global, probably via a post-processing pass
Rust doesn't have quite as many features exposed, like address_space(1) to create and access wasm globals, or -mexec-model=reactor. And Rust is more opinionated about how linking works. But I'll look into it.
That's true, but I would still much rather have this in Rust. Otherwise once we make this only one person will basically be able to modify it and this seems like it's going to be a generally useful ability to have. Ideally this would be a forcing function to motivate the addition of wasm globals or more flags to Rust, since the further C drifts from Rust's support of wasm the worse things get in general
ok I've just finished up the portion of this that will be in wit-component
which takes as input a set of preview1 functions and the preview1.wasm module and then spits out a new wasm module which is the "minimal" module to satisfy the set of preview1 functions required. Basically it takes the input wasm, asserts the shape is as expected, deletes a few exports, then runs a "gc" pass, and then outputs the final module.
The module is only mutated by adding a start
function which calls memory.grow (i32.const 1)
and sets the return value to the preview1.wasm
's stack pointer so it's got stack space
Last updated: Dec 23 2024 at 12:05 UTC