Hello all, I am working on creating a test suite for wasip3. My near-term goal is to reach wasi-http, without sockets. The longer-term goal is a conformance test that will allow an implementation of wasi to be evaluated as actually implementing wasip3.
I will be primarily testing wasmtime but also jco on top of node.
On a suggestion from @Bailey Hayes I am looking at using wasi-testsuite as a base. I will post into this topic as I go, and will probably also poke people for questions :)
So, where I'm at:
Now, these are wasip1 tests, and there's no real sense in putting effort into them, but I wonder if there isn't something to be done here; like, fopen-with-no-access asserts that errno is ENOTCAPABLE, which I gather is a very wasmtime-of-a-particular-epoch thing, and that ENOTCAPABLE isn't really a thing any more. Changing to ENOENT is more correct abstractly and allows the test to pass. This isn't limited to the C test suite, there are similar errors in the rust test suite.
Going to tag in people that may be interested in the conversation; feel free to unset notifications: @Oscar Spencer @Joel Dice @Till Schneidereit @Alex Crichton @Pat Hickey @Tim Chevalier
also @Luke Wagner
I think what I will do is, for the C tests, the source will remain the same for wasip3. i will add additional accepted errnos, and the test will remain valid for previous versions of wasi. then i will move the rust tests to a separate wasip1 subdir, because they all use the wasip1 toolchain. then i will add a wasip3 subdir to the rusts tests and start porting simple tests there, before moving on to more interesting inter-component interactions.
@Andy Wingo please tag me as well as you go.....
ENOTCAPABLE is a wasi preview 1 thing, which had a capability system called "rights" inherited from cloudabi that we decided to get rid of, not a wasmtime of era thing, but no matter. but yes, a fundamental problem here is that the tests were written in the preview 1 era and not updated since.
I agree with your approach to add more accepted errnos.
in general I was never all that happy with how the wasmtime wasi testsuite tests for behaviors that were pretty dependent on the host operating system shook out. e.g. theres flags to tell the test harness that this host doesnt support hardlinks, this host doesnt support removing nonempty dirs, things like that... basically the "is this windows or not" bit
so, after poking at it, i changed the build.sh for the c tests to be a build.py, and then i compile the generic c tests for wasm32-wasip{1,2,3} in separate directories. (wasip3 currently uses the wasip2 target triple.) there is also something to bring in version-specific tests. but to land that i am first going to update the wasi-sdk from 17 to 27, see if tests are green, move around the dirs so i can have version-specific tests, etc
i will stack some PRs. i am part of the wasm org i think so i may have some permissions there but who is the wasi-testsuite maintainer? i.e. who can lgtm?
also, who uses the daily test results?
but, in practice, filesystem behaviors on windows vs elsewhere is the least interesting problem in all of wasi - ive yet to find anyone who cares who isnt just trying to tell their boss every single tests passes, or fuzz for differences between runtimes, neither of which are very interesting things to care about. we made some severe and sometimes regrettable compromises as dan and I banged my way through creating and updating the test suite for wasmtimes needs, and this is a fine time for a reckoning of "we really dont care about minutiae of file link behaviors" when in practice several prominent runtimes that claim to "support wasi" have had filesystem sandbox escapes for many many years and have no real interest in fixing it properly
wow :)
i think historically marcin was the person to take the wasmtime wasi test suite and move it upstream into the webassembly org repos. I am happy to be your reviewer for things
daily test results? first im aware they exist tbh
there is a github action or workflow or whatever to run tests nightly and commit the results to a git branch
https://github.com/WebAssembly/wasi-testsuite/tree/prod/daily-test-results
huh. how about that
i suppose lets pull up the git blame on the action and then ask them
i'll cc them on the pr
https://github.com/WebAssembly/wasi-testsuite/issues/109 for an overview issue
a small but gnarly issue for @Pat Hickey and @Alex Crichton, https://github.com/WebAssembly/wasi-testsuite/issues/111
that IS a sticky wicket
the toolchain to create components is very funny. one would hope that that component creation sediments down into the linker at some point
Like this? https://github.com/bytecodealliance/wasm-component-ld/
ooh neat, i did not know that one :)
/me comes from the web-targetting side of wasm, not much component model stuff there
Like this? :slight_smile: https://github.com/bytecodealliance/jco/tree/main/packages/jco-transpile
i know of jco! :)
I can't disagree that it's "not much" from the outside, but it sure feels like a lot from the inside!
@Andy Wingo gets to re-prove his chops..... :-P
ahaha this is an opportunity to be a complete idiot for a while, i am also doing my first rust in anger
oh the Rust in anger part never leaves. <insert Bruce Banner I'm always angry that's my secret here />
enjoy your idiocy; we're super grateful for your help here.....
i'm happy to be here with yall!
Ah yeah this should be the default linker for wasm32-wasip2 (and a hypothetical wasm32-wasip3 future target). I'll note though that wasm-component-ld is a glorified wrapper around wasm-ld (the Real Wasm Linker) and wit-component which produces a component from a core module.
The executable should be bundled with wasi-sdk and unused on wasm32-wasip1 but used by default on wasm32-wasip2
ah i didn't realize wasm-component-ld was already in the sdk, neat. i think i was looking on https://github.com/bytecodealliance/wit-bindgen?tab=readme-ov-file#guest-cc which is probably out of date
Ah yes sorry those docs are out of date
nowadays with wasm32-wasip2 no further steps are needed after clang
does one still need -mexec-model=reactor ?
to avoid an int main entrypoint, yes
current status, working on getting some rust-based wasip3 components into wasi-testsuite. needed to brush up on the rust components toolchain; close to done. hopefully i can send some prs tomorrow.
actually, talking through this a bit -- so wasi-http depends on wasi-clocks. i will probably first try to test wasi-clocks. what is the deal with the clocks-timezone feature, will that be part of wasip3 ?
and then, for testing clocks: there is no guarantee that the clock under test corresponds to the real time. (and in some ways it would be nice in a test if the clock could tick as fast as possible; i guess i will punt on that.) anyway as regards wasmtime, will one monotonic-clock second be equal to a real second when i run tests?
and are there wasmtime tests for wasi-clocks? i was looking but haven't seen them so far
wasmtime-wasi's host API allows you to override the default clock implementations for the wall clock and monotonic clock, which we test here and here. There's also a very simple test for the default implementations here, but it doesn't assert anything very interesting at the moment.
praise be, thank you!
Regarding the timezone feature: looks like it's still an unstable feature in 0.3.0: https://github.com/WebAssembly/wasi-clocks/blob/main/wit-0.3.0-draft/timezone.wit. It could be stabilized as part of 0.3.0 or even before that in a 0.2.x release, but I don't know if anyone's working on that.
In any case, I don't think we need to include it as part of the wasi-http-centric testing.
Andy Wingo said:
actually, talking through this a bit -- so wasi-http depends on wasi-clocks. i will probably first try to test wasi-clocks. what is the deal with the clocks-timezone feature, will that be part of wasip3 ?
It is very likely that timezones will be stabilized by p3. Chatter here about steps needed before stabilization: https://github.com/WebAssembly/wasi-clocks/pull/79 @Colin D Murphy is working on updating impls and tests
thanks @Colin D Murphy !!!
i have stupid questions
regarding (2), it would seem that building cdylibs with --target=wasm32-wasip2 makes components just fine, so no need for cargo-component.
@Andy Wingo you can definitely get by without cargo component, yes. Either by just using the p2 target, or even by targeting p1 and then using a combo of wasm-tools component embed and wasm-tools component new
oh, and regarding (1), there are tools that don't yet support components. An example we're heavily reliant on with StarlingMonkey is wizer. For those cases, targeting p1, running the additional tools, then componentizing is the way to go
understood regarding -wasip1 but my comment was more about, say, x86_64-linux; weird that wit-bindgen's macros work on that target :)
I think at least one of the reasons is being able to run e.g. cargo check or cargo clippy without having to specify a target.
ah, I see! You're right that currently there are few uses for this. But not none! @Christof Petig has done a lot of work on making WIT work as an IDL for native code, for example
regarding (3), I think this should work?
CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime [cli args as needed]" cargo test
I haven't tested this in a while myself though, so needs some experimentation for sure
ooh neat
You can also put this ^ in .cargo/config.toml
Here's one from something I wrote recently (freshly tested!)
[build]
target = "wasm32-wasip2"
[target.wasm32-wasip2]
runner = "wasmtime run -D address-map"
[env]
WASMTIME_BACKTRACE_DETAILS = "1"
thanks!
Andy Wingo said:
- how should wasi-testsuite import wasi packages, e.g. wasi:clocks? git submodule? or does wit-bindgen have an appropriate search path that for some reason i haven't populated yet
ok what about this one :)
what are you all doing in your wasip3-using packages
I think wkg is the way to go for that?
ahahaha i keep learning about new parts of the toolchain
thanks!!
which is obviously entirely on you, and not due to any properties of this ecosystem :halo:
wellllllll a little of column a, a little of column b ;)
okay, I'll accept that: it's probably 10:90 instead of 0:100
The wkg UX is somewhat under-baked; if you want a full dependency tree you'll need a particular incantation combined with wasm-tools
@Lann Martin do you think we can share that incantation, or is that still a well guarded trade secret?
Working on it :slight_smile:
# This will fetch the latest version w/o deps by default
$ wkg get wasi:http
No version specified; fetching version list...
Getting wasi:http@0.3.0-rc-2025-08-15...
Wrote './wasi_http@0.3.0-rc-2025-08-15.wit'
# Deps are included in the binary encoding...
$ wkg get wasi:http@0.3.0-rc-2025-08-15 -o wasi-http.wasm
Getting wasi:http@0.3.0-rc-2025-08-15...
Wrote 'wasi-http.wasm'
# ...which can be turned into a source tree with wasm-tools
$ wasm-tools component wit wasi-http.wasm --out-dir wit
Writing: wit/deps/clocks.wit
Writing: wit/deps/random.wit
Writing: wit/deps/cli.wit
Writing: wit/http.wit
(wkg get should learn --out-dir)
am a little surprised there isn't a stream<u8> to future<wasi:http/headers> method somewhere in wasi:http
one less thing to test :)
Could you say more about what that method would do? wasi:http tries to abstract over protocol details
afaics something outside wasi:http has to turn the byte stream into http headers. i.e. you provide the headers as structured data when making a request or response
so you couldn't hook a wasi:http/handler right up to a socket, you need to write a little parser. fair enough, and it matches how some embedders will want to use it
could be i'm holding the thing wrong tho
Yeah, correct. In practice so far its mostly "whatever hyper does".
The rough idea has been that wasi:http only deals with things that are in the HTTP Semantics spec, not anything specific to HTTP/1.1,2,3+
ah yes, makes sense. i admit i have only ever implemented http/1.1
In that case I'd highly recommend staring at this specific section: httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.host
so, i am poking in a little playground repository. i have inline wit in my src/lib.rs:
wit_bindgen::generate!({inline: r"
package test:playground;
world the-world {
import wasi:http/handler;
//...
}", path="wit"});
// ...
I have imported wasi:http into wit/http.wit as above:
Writing: wit/deps/clocks.wit
Writing: wit/deps/random.wit
Writing: wit/deps/cli.wit
Writing: wit/http.wit
Firstly it would seem that it's necessary to set path in the bindgen options, otherwise cargo / wit-bindgen don't search anywhere, despite the wit-bindgen docs saying there is a default path.
Secondly, the import fails in a funny way:
error: package 'wasi:http' not found. known packages:
wasi:cli@0.3.0-rc-2025-08-15
wasi:clocks@0.3.0-rc-2025-08-15
wasi:random@0.3.0-rc-2025-08-15
wasi:http@0.3.0-rc-2025-08-15
test:playground
--> macro-input:5:20
|
5 | import wasi:http/handler;
| ^--------
--> src/lib.rs:3:1
I.e. you can see that it found wasi:http@0.3.0-rc-2025-08-15, and doesn't think that fulfills wasi:http. is wit-bindgen helpfully preventing me from using a prerelease wasip3 ?
I think you need the version in the import
yeah, import wasi:http/handler@0.3.0-rc...;
despite the wit-bindgen docs saying there is a default path
This confuses me, your syntax of path = "wit" should have failed since it should be path: "wit", but also "wit" should be the default search location (relative to the crate root)
ah, i had put the version after the handler, and didn't parse the error message correctly
Alex Crichton said:
despite the wit-bindgen docs saying there is a default path
This confuses me, your syntax of
path = "wit"should have failed since it should bepath: "wit", but also"wit"should be the default search location (relative to the crate root)
sorry, that was a retype bug: i had path: "wit". but without path: "wit", at least with this inline wit, cargo does not look in the wit/ directory
do you have a repo/branch I could repro on?
(bug hunting adventure for me)
sure i can throw it up somewhere
https://wingolog.org/priv/playground.tar.gz, extracts to playground/, repro with cargo build --target=wasm32-wasip2
aha ok inline disables the default path as otherwise if you used inline you'd have to ensure a wit directory exists -- either should update the documentation or keep the default wit path but don't require it to exist when inline is specified and path isn't specified
to be fair, who knows if inline will get much real use; but it seems nice for testing
that's the reason for existence though, it's used all the time in testing wit-bindgen itself and a few places throughout wasmtime too
basically it's too-damn-useful heh
https://github.com/bytecodealliance/wit-bindgen/pull/1356
Till Schneidereit said:
ah, I see! You're right that currently there are few uses for this. But not none! Christof Petig has done a lot of work on making WIT work as an IDL for native code, for example
FWIW, there is experimental work at my megacorp doing something like this as well....
re turning raw streams into wasi-http, our hope is that for users who want to do that, there can be a component that provides that as library code. no need for the embedder to support it
the component could be implemented internally with hyper, or with whatever other http implementation is suitable
It would be great to have a stable reference implementation for conformance testing
so, we are testing wasi implementations more than the component model itself, so i think many tests can be made just with a single component implementing wasi:cli/run. and i guess i should be able to make run() async
just as an ongoing log, a minimal test looks like this:
extern crate wit_bindgen;
wit_bindgen::generate!({inline: r"
package test:playground;
world the-world {
include wasi:cli/command@0.3.0-rc-2025-08-15;
}
", path: "wit", generate_all});
use wasi::clocks::monotonic_clock;
struct Test;
impl exports::wasi::cli::run::Guest for Test {
fn run() -> Result<(), ()> {
let start = monotonic_clock::now();
let end = monotonic_clock::now();
assert!(end <= start);
Ok(())
}
}
export!(Test);
then you have to compile wasmtime via cargo build --features component-model-async, otherwise the wit linker won't find wasip3 interfaces; and then you have to run wasmtime with -Sp3=y to enable wasip3 at run-time
this test fails as written, i had reversed end and start, just to see what would happen
now that i can build components and run them, i will start adding some to wasi-testsuite
also, a note, i am not sure how to use wkg to import a number of different packages; if you import wasi:httpas in , it will residualize some parts of wasi:clocks in wit/deps/clocks.wit; then if you import wasi:cli, it will residualize a different set of wasi:clocks into wit/deps/clocks.wit
in this current case i solved the issue by just importing wasi:cli, but that won't work once i start testing wasi:http
i suppose some of this weirdness is that i compiled a cdylib component; perhaps there are some ergonomic niceties for compiling implementations of wasi:cli/run if you make binary targets instead of libraries
Andy Wingo said:
also, a note, i am not sure how to use wkg to import a number of different packages; if you import
wasi:httpas in , it will residualize some parts ofwasi:clocksinwit/deps/clocks.wit; then if you importwasi:cli, it will residualize a different set ofwasi:clocksintowit/deps/clocks.wit
this is probably an issue to file on wkg....
I've been using wkg fetch against a dummy package / world which seems to fetch all wit dependencies of the world to write to disk. Not sure how that compares to the wkg get <interface> -o <dummy>.wasm + wasm-tools component wit <dummy>.wasm workflow.
If you manage your world in a wit/*.wit file then wkg wit fetch will indeed fetch and write deps appropriately.
tx milan & lann. for testing, where each test can define its own world inline, it would be nice to be able to fetch "all of wasi:clocks, all of wasi:sockets, ..." etc. even, all of wasip3, or wasi 1.0, ...
as it is with a dummy package, it seems i need to include each world in wasi:clocks by name
You could have a wit file that imports the root interfaces you need just for the purposes of fetching dependencies. I _think_ inline would still pick up wit/deps/ in that scenario but I can never remember the exact behaviors there
it would seem that this is enough to get all of wasi:
world wasip3 {
include wasi:cli/command@0.3.0-rc-2025-08-15;
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
include wasi:filesystem/imports@0.3.0-rc-2025-08-15;
include wasi:http/imports@0.3.0-rc-2025-08-15;
include wasi:http/proxy@0.3.0-rc-2025-08-15;
include wasi:random/imports@0.3.0-rc-2025-08-15;
include wasi:sockets/imports@0.3.0-rc-2025-08-15;
}
can just include http/proxy, it pulls in http/imports (as cli/command imports cli/imports)
anyway, more toolchain weirdness:
mkdir wasi-test
cd wasi-test
cargo init
cargo add wit-bindgen
ok, make a dummy wit file and fetch the wits:
mkdir wit
cat > wit/wasip3.wit <<EOF
package test:dummy;
world wasip3 {
include wasi:cli/command@0.3.0-rc-2025-08-15;
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
include wasi:filesystem/imports@0.3.0-rc-2025-08-15;
include wasi:http/proxy@0.3.0-rc-2025-08-15;
include wasi:random/imports@0.3.0-rc-2025-08-15;
include wasi:sockets/imports@0.3.0-rc-2025-08-15;
}
EOF
wkg wit fetch
It takes a little time but does make some wit files:
$ find wit
wit
wit/wasip3.wit
wit/deps
wit/deps/wasi-sockets-0.3.0-rc-2025-08-15
wit/deps/wasi-sockets-0.3.0-rc-2025-08-15/package.wit
wit/deps/wasi-random-0.3.0-rc-2025-08-15
wit/deps/wasi-random-0.3.0-rc-2025-08-15/package.wit
wit/deps/wasi-http-0.3.0-rc-2025-08-15
wit/deps/wasi-http-0.3.0-rc-2025-08-15/package.wit
wit/deps/wasi-clocks-0.3.0-rc-2025-08-15
wit/deps/wasi-clocks-0.3.0-rc-2025-08-15/package.wit
wit/deps/wasi-filesystem-0.3.0-rc-2025-08-15
wit/deps/wasi-filesystem-0.3.0-rc-2025-08-15/package.wit
wit/deps/wasi-cli-0.3.0-rc-2025-08-15
wit/deps/wasi-cli-0.3.0-rc-2025-08-15/package.wit
Now make src/main.rs:
extern crate wit_bindgen;
wit_bindgen::generate!({inline: r"
package test:test;
world test {
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
}
", path: "wit", generate_all});
use wasi::clocks::monotonic_clock;
fn main() {
let start = monotonic_clock::now();
let end = monotonic_clock::now();
assert!(start <= end);
}
Try to build:
cargo build --release --target=wasm32-wasip2
Failures in wit-bindgen:
error: failed to resolve directory while parsing WIT for path [/home/wingo/src/wasip3/wasi-test/wit]
Caused by:
interface not found in package
--> /home/wingo/src/wasip3/wasi-test/wit/deps/wasi-cli-0.3.0-rc-2025-08-15/package.wit:157:22
|
157 | import wasi:clocks/timezone@0.3.0-rc-2025-08-15;
| ^-------
...
Which is weird because timezones are behind an unstable feature? OK, add features: ["clocks-timezone"] to the generate! invocation:
wit_bindgen::generate!({inline: r"
package test:test;
world test {
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
}
", path: "wit", features: ["clocks-timezone"], generate_all});
Another weird wit-bindgen error:
error: multiple packages have a world, must specify which to use
Which, fair, there are other packages in wit/, but I gave it inline WIT, it should know to choose that world. Once you do this, it works:
wit_bindgen::generate!({inline: r"
package test:test;
world test {
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
}
", path: "wit", features: ["clocks-timezone"], world: "test:test/test", generate_all});
And with that, it's the shortest sync test I can make. The resulting component still imports some wasip2 interfaces, so this is not a pure wasip3 test:
$ wasm-tools component wit target/wasm32-wasip2/release/playground.wasm
package root:component;
world root {
import wasi:clocks/monotonic-clock@0.3.0-rc-2025-08-15;
import wasi:cli/environment@0.2.3;
import wasi:cli/exit@0.2.3;
import wasi:io/error@0.2.3;
import wasi:io/streams@0.2.3;
import wasi:cli/stdin@0.2.3;
import wasi:cli/stdout@0.2.3;
import wasi:cli/stderr@0.2.3;
import wasi:clocks/wall-clock@0.2.3;
import wasi:filesystem/types@0.2.3;
import wasi:filesystem/preopens@0.2.3;
export wasi:cli/run@0.2.3;
}
But, fair enough, there isn't even a wasm32-wasip3 target for rustc yet.
cc @Alex Crichton for some of the wit-bindgen oddness
interface not found in package
We've had a lot of historical weirdness around include and feature-gated interfaces so this might be a legitimate bug there. If you avoid include-ing the imports interface and import the relevant ones directly I think it might work?
multiple packages have a world, must specify which to use
I think that may be due to my fix yesterday where with path plus inline it's thinking that there's two "root packages" to choose worlds from, I'll double check the behavior.
The resulting component still imports some wasip2 interfaces, so this is not a pure wasip3 test:
That's expected yeah, you're running into Rust's usage of WASIp1 in the standard library which gets adapted to WASIp2 through the adapter baked into wasm-component-ld
Alex Crichton said:
interface not found in package
We've had a lot of historical weirdness around
includeand feature-gated interfaces so this might be a legitimate bug there. If you avoidinclude-ing theimportsinterface andimportthe relevant ones directly I think it might work?
Seems to not work; if after fetching I remove the wasip3.wit and change the inline wit to import wasi:clocks/monotonic-clock@0.3.0-rc-2025-08-15; instead of including, I still get the same error, which actually originates inside the wkg-imported package:
interface not found in package
--> /home/wingo/src/wasip3/wasi-test/wit/deps/wasi-cli-0.3.0-rc-2025-08-15/package.wit:157:22
|
157 | import wasi:clocks/timezone@0.3.0-rc-2025-08-15;
hm there may be other packages doing include wasi:clocks/imports maybe, I'll try to dig in in a bit (I need to learn wkg myself...)
(i wasn't sure if wkg would mangle the wit to either fabricate or elide the "include", but in this case it does seem to preserve the include)
yeah I'll need to double-check how unstable things all work here, what you're doing should work but unstable features have been a thorn in our side for quite awhile
it is not a blocker for me, i just wanted to write it down so i won't forget :)
indeed, and thanks!
thank you!
otherwise today i talked a bit with @Marcin Kolny regarding wasi-testsuite organization, we seem to be on the same page regarding making wasi-testsuite useful for wasip3 and the broad lines of how to get there. will have some prs up tomorrow.
avoiding the need to specify world:"..." when you've also specified inline should be fixed in https://github.com/bytecodealliance/wit-bindgen/pull/1358
stupid question: would async fn main() make sense in wasip3, for binary targets? (of course it doesn't work. but it seems like it would be nice)
it would, yeah, but probably won't get native language support for awhile
i guess this would involve more work on the rustc toolchain, whereas building cdylibs can be a library thing
what we could get working though is:
#[wit_bindgen::main]
async fn main() { ... }
which is similar to #[tokio::main] used in a number of locations
ah interesting
Presumably you'd still have to build that as cdylib?
would you? binary targets seem to produce working components without any special annotation in the Cargo.toml
technically you wouldn't have to, but you likely would yeah
(which makes my idea probably not work)
it'd produce a workable component but it'd be "async stackful" in a sense instead of "async callback"
/me nod
In case it's not obvious, you can certainly export an async wasi:cli/run#run function using wit-bindgen today with a bit of boilerplate, e.g. https://github.com/bytecodealliance/wasmtime/blob/main/crates/test-programs/src/bin/p3_cli.rs
ok the other unstable-related issue is confirmed and minimized as:
package a:b1;
world the-world {
include a:b2/the-world;
}
package a:b2 {
world the-world {
@unstable(feature = disabled)
import a:b3/thing;
}
}
package a:b3 {
@unstable(feature = disabled)
interface thing {}
}
(that fails as input to wasm-tools component wit)
Would it be possible to have a
#[wit_bindgen::test]
async fn test_xxx { ... }
?
yeah we could probably get that working
(filed a bug here)
so, wasi-testsuite and wasmtime: there are some tests that fail on rust for --target=wasm32-wasip1. i triaged here: https://github.com/WebAssembly/wasi-testsuite/issues/101#issuecomment-3210744361. if there is a kind soul who is able to triage, that would be helpful
daily status: spent some time prepping wasi-testsuite for wasip3 tests: https://github.com/WebAssembly/wasi-testsuite/issues/109#issuecomment-3210363725. following up on marcin's review comments.
in the process i triaged the wasip1 tests mentioned in
so, it was a day of plumbing. but, fine, hopefully tomorrow i add the wasip3 things
https://github.com/WebAssembly/wasi-testsuite/issues/117
so, is wasi-testsuite the right place for wasip3 tests?
i want to focus on wasip3 tests and there has probably too much "making space for wasip3" so far
I think it might be okay to focus on wasip3 for expedience's sake for now, but long-term, it definitely is the right place, yes: once published, p3 will be the latest version of WASI, which should be reflected in the standard's test suite :slight_smile:
ok. i will just keep piling up the prs, hopefully one day things will go faster
I think some refactoring is required at the beginning as wasi-testsuite wasn't designed initially for multiple wasi versions. Once we'll go through that phase, further development should be easier.
wasip1 tests found a nice wasmtime bug: https://github.com/WebAssembly/wasi-testsuite/issues/101#issuecomment-3214123363
hmm, i am a bit perplexed. consider this file:
extern crate wit_bindgen;
wit_bindgen::generate!({
inline: r"
package test:test;
world test {
include wasi:clocks/imports@0.3.0-rc-2025-08-15;
include wasi:cli/command@0.3.0-rc-2025-08-15;
}
",
// Work around https://github.com/bytecodealliance/wasm-tools/issues/2285.
features:["clocks-timezone"],
async: [
"wasi:cli/run@0.3.0-rc-2025-08-15#run",
],
generate_all
});
use wasi::clocks::monotonic_clock;
struct Component;
export!(Component);
impl exports::wasi::cli::run::Guest for Component {
async fn run() -> Result<(), ()> {
monotonic_clock::wait_for(50_000u64).await;
Ok(())
}
}
fn main() {}
placed in src/bin/minimal.rs, there are the needed wit files in wit/. Build via cargo build --target=wasm32-wasip2 --release --bin minimal, with wit-bindgen from git. It has an error:
[dev-env] wingo@beastie ~/src/wasip3/wasi-testsuite/tests/rust/wasm32-wasip3$ cargo build --target=wasm32-wasip2 --release --bin minimal
Compiling test-wasm32-wasip3 v0.1.0 (/home/wingo/src/wasip3/wasi-testsuite/tests/rust/wasm32-wasip3)
error: linking with `wasm-component-ld` failed: exit status: 1
|
= note: "wasm-component-ld" "-flavor" "wasm" "--export" "[async-lift]wasi:cli/run@0.3.0-rc-2025-08-15#run" "--export" "[callback][async-lift]wasi:cli/run@0.3.0-rc-2025-08-15#run" "--export" "__main_void" "--export" "cabi_realloc" "-z" "stack-size=1048576" "--stack-first" "--allow-undefined" "--no-demangle" "<sysroot>/lib/rustlib/wasm32-wasip2/lib/self-contained/crt1-command.o" "<2 object files omitted>" "/home/wingo/src/wasip3/wasi-testsuite/tests/rust/wasm32-wasip3/target/wasm32-wasip2/release/deps/{libwit_bindgen-d3f7658c15e3bc1f.rlib,libwit_bindgen_rt-e215225e28d37754.rlib,libfutures-1e3aa9526458c95b.rlib,libfutures_executor-4c14e82b92128eea.rlib,libfutures_util-d4d1ebf3054ace8e.rlib,libmemchr-2857ac1428ecf686.rlib,libfutures_io-bd6f5f056e177761.rlib,libslab-b3944246b67457c3.rlib,libfutures_channel-af04b86abb9667b5.rlib,libpin_project_lite-38e8286ac72dc06c.rlib,libfutures_sink-87533dcfb4185e08.rlib,libfutures_task-4934eebbf3d64f90.rlib,libpin_utils-1407d4781aab0b76.rlib,libfutures_core-c2a2480a6530cd71.rlib,libbitflags-77083259b2ec5fc4.rlib}.rlib" "<sysroot>/lib/rustlib/wasm32-wasip2/lib/{libstd-*,libpanic_abort-*,libwasi-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,libcfg_if-*,liblibc-*}.rlib" "-l" "c" "<sysroot>/lib/rustlib/wasm32-wasip2/lib/{librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib" "-L" "/home/wingo/src/wasip3/wasi-testsuite/tests/rust/wasm32-wasip3/target/wasm32-wasip2/release/build/wit-bindgen-rt-f69b726d7f89fc43/out" "-L" "<sysroot>/lib/rustlib/wasm32-wasip2/lib/self-contained" "-o" "/home/wingo/src/wasip3/wasi-testsuite/tests/rust/wasm32-wasip3/target/wasm32-wasip2/release/deps/minimal-83a77969722bd283.wasm" "--gc-sections" "-O3" "--strip-debug"
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: error: failed to encode component
Caused by:
0: failed to decode world from module
1: module was not valid
2: failed to resolve import `wasi:clocks/monotonic-clock@0.3.0-rc-2025-08-15::[async-lower][async]wait-for`
3: failed to validate import interface `wasi:clocks/monotonic-clock@0.3.0-rc-2025-08-15`
4: type mismatch for function `[async]wait-for`: expected `[I32, I32] -> [I32]` but found `[I64] -> [I32]`
If i switch to simple assertions about monotonic_clock::now(), that works, so it's not a linking error for sync bindings. In what is probably a second problem, the resulting file fails to run:
$ "$HOME/src/wasip3/wasmtime/target/release/wasmtime" -Sp3=y target/wasm32-wasip2/release/minimal.wasm
Error: failed to parse WebAssembly module
Caused by:
`task.return` requires the component model async feature (at offset 0x17b99)
Which is odd because I built wasmtime with --features component-model-async. I will try building this as a cdylib instead, perhaps that will help.
right, I get the same result for a minimal cdylib:
extern crate wit_bindgen;
wit_bindgen::generate!({
inline: r"
package test:playground;
world the-world {
include wasi:cli/command@0.3.0-rc-2025-08-15;
}
",
path: "wit",
async: [
"wasi:cli/run@0.3.0-rc-2025-08-15#run",
],
generate_all
});
struct Test;
impl exports::wasi::cli::run::Guest for Test {
async fn run() -> Result<(), ()> {
Ok(())
}
}
export!(Test);
task.return requires the component model async feature
I believe this requires wasmtime -W component-model-async (runtime feature gate too)
oh funny, i didn't think to look there, like "wasm" was a lower-level thing than wasi / component-model. thanks!
Yeah we aren't generally very precise in distinguishing these things but technically component model is a wasm proposal while wasi is its own separate (dependent) spec
since they are developed in parallel it is convenient to do lockstep-ish releases and they both end up in the "p3" bucket
what do i need to enable to fix invalid leading byte (0x25) for canonical function (at offset 0x16a4a) ?
i had tried all-proposals=y in addition to component-model-async=y but that wasn't the trick
Ah I just encountered that one. I believe that is caused by a mismatched wasm-component-ld version
I fixed it by switching to a recent rust nightly
oh :)
wouldn't that be something in wasi-sdk?
ah perhaps i wasn't running my cargo builds in an environment that had my fresh wasi-sdk
wasi-sdk shouldn't have anything to do with binary encoding
well wasm-component-ld is there
but maybe wasi-sdk is only for c?
weird stuff
ah wasi-sdk might include wasm-component-ld to make sure you get a compatible version
wasi-sdk can be used by any guest language that can do C ffi in wasm
its basically wasi-libc plus a compatible toolchain
well many thanks lann, after rustupping to a nightly, things are working now
nice, all problems fixed. ready to make tests, finally ;)
a first async test at https://github.com/WebAssembly/wasi-testsuite/pull/120, grumble about github and stacked pr's tho
/me files a spicy bug https://github.com/WebAssembly/wasi-http/issues/179
just as a summary of where i'm at:
prod/testsuite-base branch with prebuilt testsclocks:monotonic-clockclocks:wall-clock (though there is very little that one can assert there), and it adds a test with 20 outstanding monotonic-clock#wait-until invocations, asserting that they complete in order with regards to each other and to the clockthanks for lots of great work and great questions, andy!
good morning :) i would like to merge https://github.com/WebAssembly/wasi-testsuite/pull/120 today
how should i proceed here @Marcin Kolny ?
status: i have a 60%-done test for wasi:filesystem, but am not posting a pr yet because i am waiting on https://github.com/WebAssembly/wasi-testsuite/pull/120; otherwise there is nowhere to put it
current status: slogging away at https://github.com/WebAssembly/wasi-testsuite/pull/130/files. i have done almost everything that doesn't require streams. starting streams now; found a fun wasmtime bug: https://github.com/bytecodealliance/wasmtime/issues/11652. will continue tomorrow
As a status update on the wasmtime side of things, Andy now wasi:http bits are all landed in Wasmtime so the dev release should be suitable for testing all of WASIp3
@Joel Dice no idea how, but the stream/future host API refactor (https://github.com/bytecodealliance/wasmtime/pull/11515) fixed my iloop in readdir (https://github.com/bytecodealliance/wasmtime/issues/11652#issuecomment-3278931677). Weird!
hoo, read-from-stream's prototype is weird
i guess it is to be expected that this hangs:
let (stream, future) = fd.read_via_stream(0);
future.await
I have had the same confusion recently. I think in general we expect to read the stream until it's dropped, then poll the future.
(or do both concurrently)
one of the weird things is that it's tricky to test that read-via-stream(u64::MAX) fails, because if you drop the stream immediately, the future doesn't necessarily resolve to an error
do zero-length reads still allow the stream to start?
it seems not
I think they're supposed to, or at least were supposed to in some design iteration
The doc comment in wasmtime main right now says that a zero length read will only return pending if it "expects" that a non-zero-length read would also return pending.
That's for host streams in general; the filesystem impl might not be up-to-date with that interpretation
Andy Wingo said:
no idea how, but the stream/future host API refactor (https://github.com/bytecodealliance/wasmtime/pull/11515) fixed my iloop in readdir
That's not totally surprising; that refactor was primarily motivated by the need for finer-grained, more predictable, control of streams and futures in wasmtime-wasi[-http], and it also forced us to think more precisely about how we wanted them to behave.
Zero-length reads/writes are a bit weird because they're used for readiness checks where possible, but a 1-byte read at u64::MAX - 1 should deterministically fail
AbiBuffer is odd: its remaining() method would seem to duplicate StreamResult::Complete. as a user you get both values. i get the reason but it's weird
and my goodness, writing to a stream is gnarly
so. component-model-knowers. i initially wrote this to test write-via-stream:
async fn pwrite(fd: &Descriptor, offset: u64, data: &[u8]) -> Result<usize, ErrorCode> {
let (mut tx, rx) = wit_stream::new();
let success = fd.write_via_stream(rx, offset);
let len = data.len();
let mut written: usize = 0;
let (mut result, mut buf) = tx.write(data.to_vec()).await;
loop {
match result {
StreamResult::Complete(n) => {
assert!(n <= len - written);
written += n;
assert_eq!(buf.remaining(), len - written);
if buf.remaining() != 0 {
(result, buf) = tx.write_buf(buf).await;
} else {
break;
}
},
StreamResult::Dropped => {
panic!("receiver dropped the stream?");
},
StreamResult::Cancelled => {
break;
}
}
}
assert_eq!(buf.remaining(), len - written);
drop(tx);
match success.await {
Ok(()) => Ok(written),
Err(err) => Err(err)
}
}
could be buggy, excuse my bad rust form, etc; anyway, when running this, wasmtime deadlocks. helpfully it detects the deadlock; but it is not clear to me why it should deadlock at all. i wrote this from the spec and docs, but now looking at wasmtime tests it would seem that it wants me to wait in parallel on the individual stream writes, at the same time as i wait on the write-via-stream results: https://github.com/bytecodealliance/wasmtime/blob/main/crates/test-programs/src/bin/p3_file_write.rs#L45
but why is this? if there is an error, why wouldn't the receiving end cancel the stream and cause the future to complete with a failure?
the deadlock occurs at the tx.write(data.to_vec()).await line
Lann Martin said:
I have had the same confusion recently. I think in general we expect to read the stream until it's dropped, then poll the future.
this is what I was expecting for the write case, then: write the stream until complete or cancelled, then poll the future. is it necessarily not that way?
Just to clarify terminology, completed and cancelled are the two stream results that - maybe confusingly - don't terminate the stream. Otherwise yes, my expectation is that you write to the stream until you either get a dropped result (typically because of a write error) or you drop it yourself (because you are done), then await the future.
hoo, i did not realize that cancelled doesn't terminate the stream
yeah it is just cancelling the one active operation on the stream
Take this with a grain of salt until Joel/Alex/Luke can confirm but I believe the state diagram is basically this: https://gist.github.com/lann/d8dbc6feaf36eb70a17e9506e3c06597
@Andy Wingo I'm not sure what's causing the deadlock. Would you mind providing instructions for reproducing the issue, e.g. a complete Rust file I can run using wasmtime run? BTW, you could use StreamWriter::write_all and avoid the loop.
@Lann Martin your diagram looks roughly accurate, but note that the read and write ends of a stream are dropped separately, so a stream could be in one of three states: where neither end is dropped, the write end is dropped, or the read end is dropped. And when both ends are dropped it ceases to exist entirely.
Yeah I should clarify that I was only trying to represent the writer's perspective there.
as a user you get both values. i get the reason but it's weird
This is trying to expose what the component model is giving us in that the return code of an async operation indicates how many items were transferred and the end-result status of the operation. In many cases dropped == 0 items taken for example but that's not guaranteed.
and my goodness, writing to a stream is gnarly
IMO I'd view this operation as a kernel-like operation. Managing epoll and/or trying to use io-uring would also be quite gnarly and I wouldn't expect those to be simple. I'd think of raw CM streams similarly.
I've tried to add a few helper methods like write_all and such and if you end up having other common operations it's fine to add more helper methods too.
when running this, wasmtime deadlocks
This is a subtle difference between Rust & JS, calling fd.write_via_stream(rx, offset) doesn't actually do anything. You have to actually poll the future, which in this example only happens at the end of the function during success.await, for anything to happen. The reason your example deadlocks is that functionally what's happening is you created a stream and then wrote to it, but nothing is reading from it because the host API hasn't actually been called yet.
In the test in Rust you'll find usage of a futures::join! macro which is the current solution for this. It represents "do these two things at the same time" which is what you want here, one end calls success.await and the other ened does the writing.
Joel/Alex/Luke can confirm but I believe the state diagram is basically this: https://gist.github.com/lann/d8dbc6feaf36eb70a17e9506e3c06597
FWIW that looks right to me
Yeah to your point above, in that state diagram all of the results are implicitly from polling something.
Alex Crichton said:
as a user you get both values. i get the reason but it's weird
This is trying to expose what the component model is giving us in that the return code of an async operation indicates how many items were transferred and the end-result status of the operation. In many cases dropped == 0 items taken for example but that's not guaranteed.
So let's say you have a stream, you have a vector of total-count items to write the stream, and the receiver consumes those items in a number of batches.
My question: from the transmitter's perspective (i.e., when it is active and not suspended), could we calculate the AbiBuffer's remaining() as the total-count minus the sum of the previously-received StreamResult::Complete(n) values? That is what I meant by duplication. But you seem to be suggesting that no, if the stream result is Dropped, that the previous write may have only been partially consumed, and that the remaining() is the real source of truth? I would have expected a Complete(n) for the partial write, then a Dropped.
and my goodness, writing to a stream is gnarly
IMO I'd view this operation as a kernel-like operation. Managing epoll and/or trying to use io-uring would also be quite gnarly and I wouldn't expect those to be simple. I'd think of raw CM streams similarly.
I've tried to add a few helper methods like
write_alland such and if you end up having other common operations it's fine to add more helper methods too.
I would certainly recommend the write_all methods to others, but I am just trying to get a feel for how things can break when there are drops and cancels and so on :)
when running this, wasmtime deadlocks
This is a subtle difference between Rust & JS, calling
fd.write_via_stream(rx, offset)doesn't actually do anything. You have to actually poll the future, which in this example only happens at the end of the function duringsuccess.await, for anything to happen.
This is indeed subtle, thank you for the hint! This entirely explains it. I had no idea about this aspect of Rust, the systems I have worked with all begin execution of the subtask immediately and only suspend if necessary. Thanks again for the generous close read.
Joel Dice said:
Andy Wingo I'm not sure what's causing the deadlock. Would you mind providing instructions for reproducing the issue, e.g. a complete Rust file I can run using
wasmtime run? BTW, you could useStreamWriter::write_alland avoid the loop.
I had just finished preparing the example when Alex corrected my misunderstanding, no need now. Thank you though.
@Alex Crichton question, how do you know that in Rust, fd.read_via_stream won't have to poll the future before the sender will be active?
i assume that the whole pre-poll sendable Future thing is a rust-only concern: it would seem that on a component-model async ABI level, calling an async import actually starts the task (as in JS, C#, etc)
Should you be interested, I think this classic blog post by Aaron Turon still gives a pretty good explanation for the design of Rust Futures
i also enjoyed the blandy/orendorff/tindall chapter on asyncs
finishing filesystem tests. i am mostly assuming i can read my own writes, that writes to a descriptor within a component are immediately visible to reads within that component without sync, both for data and stat().size
Andy Wingo said:
i assume that the whole pre-poll sendable Future thing is a rust-only concern: it would seem that on a component-model async ABI level, calling an async import actually starts the task (as in JS, C#, etc)
I think this is understood but just to be completely clear: in your example the import isn't called until the Rust Future is polled. If you are more familiar with JS you can think of Rust async functions as generators.
i know that wasi isn't much able to specify this but maybe tests can nudge implementations in the right direction, i.e. that adding buffering on the host side is a bad thing
Lann Martin said:
Andy Wingo said:
i assume that the whole pre-poll sendable Future thing is a rust-only concern: it would seem that on a component-model async ABI level, calling an async import actually starts the task (as in JS, C#, etc)
I think this is understood but just to be completely clear: in your example the import isn't called until the Rust Future is polled. If you are more familiar with JS you can think of Rust async functions as generators.
i did not understand this before but after alex's comment i do now!
(don't js generators actually get to run a bit immediately? i can't remember, and i implemented them in spidermonkey and v8.. ;)
They do not (I had to verify in the console :sweat_smile:)
live and learn (and forget, and learn...) :)
I guess more to your question of whether its a "rust-only concern", that depends on whether any other languages also treat async functions as generators. I think Python might (edit: it does)? But if you are just asking about the ABI, yes once the async call reaches the ABI the task starts immediately.
Andy Wingo said:
finishing filesystem tests. i am mostly assuming i can read my own writes, that writes to a descriptor within a component are immediately visible to reads within that component without sync, both for data and stat().size
i know that wasi isn't much able to specify this but maybe tests can nudge implementations in the right direction, i.e. that adding buffering on the host side is a bad thing
My non-authoritative take would be that for a single descriptor yes you should be able to read your own writes.
(feedback welcome re the sync/buffering thing)
So let's say you have a stream, you have a vector of total-count items to write the stream, and the receiver consumes those items in a number of batches.
I'm not entirely sure how to respond to your question here in that I'm not totally sure I understand it. That being said I'm going to make a statement here which you may already be aware of but in case you weren't it might reframe your questions here: if a writer writes 10 items, and a reader then reads 4 items, the writer will receive a notification that 4 items were read. The writer doesn't, for example, say blocked until all 10 items are read.
There's room in the CM spec to allow the reader to keep reading before a notification is delivered (e.g. you read one-item-at-a-time in a loop and we batch all the notifications for the writer) but that's not implemented in Wasmtime right now.
if the stream result is
Dropped, that the previous write may have only been partially consumed, and that theremaining()is the real source of truth?
This is indeed a possibility, yeah. Although reading over the APIs again it's a bug that Dropped doesn't have a usize payload on it, that definitely should.
With that combo of words, does that answer your original question? I fear it may not... Let me know though!
how do you know that in Rust,
fd.read_via_streamwon't have to poll the future before the sender will be active?
Can confirm others' answers here, it's purely a Rust thing. When you do foo() where it was defined as async fn foo() { println!("hi") } that does nothing and prints nothing. All it does is construct a future on the stack that, when polled, will actually print. Once we hit the CM, however, then everything happens in the background similar to JS/etc and pollling in Rust need not happen other than to actually acquire the result of the operation when it's done.
also I totally empathize with you implementing JS generators, my friend and I wrote a JIT for lua in college and I don't actually know anything about writing lua
Alex Crichton said:
There's room in the CM spec to allow the reader to keep reading before a notification is delivered (e.g. you read one-item-at-a-time in a loop and we batch all the notifications for the writer) but that's not implemented in Wasmtime right now.
I believe it is implemented, per https://github.com/bytecodealliance/wasip3-prototyping/issues/162 and https://github.com/bytecodealliance/wasip3-prototyping/pull/168
oh nice!
I thought that was just some hypothetical future thing we would do at some point
I think the hypothetical future thing is to allow you to start multiple reads or writes on the same stream without waiting for previous ones to complete, which is different.
i.e. you can start a new read for a write you already read from, but only after you get notification that the previous read completed
(and assuming the writer hasn't received their notification yet, at which point the reader will need to wait for a new write)
Regarding what Alex Crichton said, I think I was missing that Dropped logically has a usize associated with it:
if the stream result is
Dropped, that the previous write may have only been partially consumed, and that theremaining()is the real source of truth?This is indeed a possibility, yeah. Although reading over the APIs again it's a bug that
Droppeddoesn't have ausizepayload on it, that definitely should.
Alex Crichton said:
Once we hit the CM, however, then everything happens in the background similar to JS/etc and pollling in Rust need not happen other than to actually acquire the result of the operation when it's done.
To clarify, though: it's all still cooperative, so if the caller goes into an infinite loop without yielding, the callee will not have a chance to make progress. I.e. "in the background" means, "when nothing else is monopolizing the single thread of control".
is the cooperative nature observable? can progress be made on a cross-component async call in another thread, invisible to other components?
observable, yes, cross-thread, no
ack
ok! added my filesystem tests, i think especially as regards the actual streaming i/o there can be more tests, but at least all the api is covered. a bunch of similar draft pr's up at https://github.com/WebAssembly/wasi-testsuite/pulls, but only one marked as ready, so that changes from that review can be replicated in the others: https://github.com/WebAssembly/wasi-testsuite/pull/131
the tests get compiled nightly but are not run yet. now that wasi-testsuite actually has wasip3 tests, i need to wire up that bit of the CI.
will see what i can do with http, now that it's in.
review welcome on the wasi:filesystem tests: https://github.com/WebAssembly/wasi-testsuite/pulls
OK, quick status: the wasi-testsuite runner tests multiple runtimes at once and skips e.g. WASIp3 tests on a runtime that just does WASIp1. Working on filing off some sharp edges. Tomorrow I will add a jco adapter. There are tests basically only for wasi:clocks and wasi:filesystem currently, and most of the filesystem ones are out for review still; I have held off on http until https://github.com/bytecodealliance/wasmtime/issues/11736 is fixed.
but generally speaking see https://github.com/WebAssembly/wasi-testsuite/pull/162 for an idea of status
Last updated: Dec 06 2025 at 06:05 UTC