From 42d7884fb1fc751c83e274db2c7ef89959f670fb Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Tue, 1 Sep 2026 21:09:29 +0000 Subject: [PATCH 01/19] add p3 feature --- Cargo.toml | 70 ++++++++++++++++++++++++++++++++++++++++++++++--- axum/Cargo.toml | 19 +++++++++++++- axum/src/lib.rs | 1 + src/lib.rs | 40 +++++++++++++++++++++------- 4 files changed, 116 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 386afa8..3b4a3b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,13 @@ repository.workspace = true rust-version.workspace = true [features] -default = ["json"] +default = ["json", "p2"] json = ["dep:serde", "dep:serde_json"] +# WASI backend selection. Exactly one of these must be enabled. +p2 = ["dep:wasip2"] +p3 = ["dep:wasip3"] + [dependencies] anyhow.workspace = true async-task.workspace = true @@ -27,9 +31,11 @@ http.workspace = true itoa.workspace = true pin-project-lite.workspace = true slab.workspace = true -wasip2.workspace = true wstd-macro.workspace = true +wasip2 = { workspace = true, optional = true } +wasip3 = { workspace = true, optional = true } + # optional serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } @@ -43,6 +49,62 @@ humantime.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +[[test]] +name = "http_first_byte_timeout" +required-features = ["p2"] + +[[test]] +name = "http_get" +required-features = ["p2"] + +[[test]] +name = "http_get_json" +required-features = ["p2"] + +[[test]] +name = "http_handle_error_code" +required-features = ["p2"] + +[[test]] +name = "http_post" +required-features = ["p2"] + +[[test]] +name = "http_post_json" +required-features = ["p2"] + +[[test]] +name = "http_timeout" +required-features = ["p2"] + +[[test]] +name = "sleep" +required-features = ["p2"] + +[[example]] +name = "complex_http_client" +required-features = ["p2"] + +[[example]] +name = "http_client" +required-features = ["p2"] + +[[example]] +name = "http_server" +required-features = ["p2"] + +[[example]] +name = "http_server_proxy" +required-features = ["p2"] + +[[example]] +name = "tcp_echo_server" +required-features = ["p2"] + +[[example]] +name = "tcp_stream_client" +required-features = ["p2"] + [workspace] members = [ "axum", @@ -95,13 +157,13 @@ test-programs = { path = "test-programs" } tower-service = "0.3.3" ureq = { version = "3.1", default-features = false, features = ["json"] } wasip2 = "1.0" -wstd = { path = ".", version = "=0.6.8" } +wasip3 = "0.8" +wstd = { path = ".", version = "=0.6.8", default-features = false } wstd-axum = { path = "./axum", version = "=0.6.8" } wstd-axum-macro = { path = "./axum/macro", version = "=0.6.8" } wstd-macro = { path = "./macro", version = "=0.6.8" } [package.metadata.docs.rs] -all-features = true targets = [ "wasm32-wasip2" ] diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 154a021..baa5da4 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -13,12 +13,29 @@ rust-version.workspace = true [dependencies] axum.workspace = true tower-service.workspace = true -wstd.workspace = true +wstd = { workspace = true, features = ["json"] } wstd-axum-macro.workspace = true +[features] +default = ["p2"] +p2 = ["wstd/p2"] +p3 = ["wstd/p3"] + [dev-dependencies] anyhow.workspace = true futures-concurrency.workspace = true serde = { workspace = true, features = ["derive"] } serde_qs.workspace = true axum = { workspace = true, features = ["query", "json", "macros"] } + +[[example]] +name = "hello_world" +required-features = ["p2"] + +[[example]] +name = "hello_world_nomacro" +required-features = ["p2"] + +[[example]] +name = "weather" +required-features = ["p2"] diff --git a/axum/src/lib.rs b/axum/src/lib.rs index 3272b91..9536c89 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "p2")] //! Support for the [`axum`] web server framework in wasi-http components, via //! [`wstd`]. //! diff --git a/src/lib.rs b/src/lib.rs index ebc673d..0dc8ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ //! **TCP echo server** //! //! ```rust,no_run -#![doc = include_str!("../examples/tcp_echo_server.rs")] +#![cfg_attr(feature = "p2", doc = include_str!("../examples/tcp_echo_server.rs"))] //! ``` //! //! **HTTP Client** @@ -30,7 +30,7 @@ //! **HTTP Server** //! //! ```rust,no_run -#![doc = include_str!("../examples/http_server.rs")] +#![cfg_attr(feature = "p2", doc = include_str!("../examples/http_server.rs"))] //! ``` //! //! # Design Decisions @@ -55,30 +55,52 @@ //! These are unique capabilities provided by WASI 0.2, and because this library //! is specific to that are exposed from here. +// Exactly one WASI backend must be selected. See the `p2`/`p3` features. +#[cfg(all(feature = "p2", feature = "p3"))] +compile_error!( + "the `p2` and `p3` features are mutually exclusive — enable exactly one WASI backend" +); +#[cfg(not(any(feature = "p2", feature = "p3")))] +compile_error!("exactly one of the `p2` or `p3` features must be enabled"); + +#[cfg(feature = "p2")] pub mod future; +#[cfg(feature = "p2")] #[macro_use] pub mod http; +#[cfg(feature = "p2")] pub mod io; pub mod iter; +#[cfg(feature = "p2")] pub mod net; +#[cfg(feature = "p2")] pub mod rand; +#[cfg(feature = "p2")] pub mod runtime; +#[cfg(feature = "p2")] pub mod task; +#[cfg(feature = "p2")] pub mod time; -pub use wstd_macro::attr_macro_http_server as http_server; -pub use wstd_macro::attr_macro_main as main; -pub use wstd_macro::attr_macro_test as test; +#[cfg(feature = "p2")] +pub use wstd_macro::{ + attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, +}; -// Re-export the wasip2 crate for use only by `wstd-macro` macros. The proc -// macros need to generate code that uses these definitions, but we don't want -// to treat it as part of our public API with regards to semver, so we keep it -// under `__internal` as well as doc(hidden) to indicate it is private. +// Re-export the active WASI backend crate for use only by `wstd-macro` macros. +// The proc macros need to generate code that uses these definitions, but we +// don't want to treat it as part of our public API with regards to semver, so +// we keep it under `__internal` as well as doc(hidden) to indicate it is +// private. #[doc(hidden)] pub mod __internal { + #[cfg(feature = "p2")] pub use wasip2; + #[cfg(feature = "p3")] + pub use wasip3; } +#[cfg(feature = "p2")] pub mod prelude { pub use crate::future::FutureExt as _; pub use crate::io::AsyncRead as _; From 4a6e65cc71b452d39000f998e47c04e605c38dd0 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 2 Sep 2026 12:24:53 +0000 Subject: [PATCH 02/19] add p3 test CI job --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1362609..9681c60 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,6 +54,9 @@ jobs: - name: wstd tests run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 -- --nocapture + - name: p3 wstd tests + run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 --no-default-features --features p3 -- --nocapture + - name: test-programs tests run: cargo test -p test-programs -- --nocapture if: steps.creds.outcome == 'success' From ea32edf32998964ef18ab766043b98a226fbf5f4 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Thu, 3 Sep 2026 14:00:43 +0000 Subject: [PATCH 03/19] use cfg alias --- Cargo.toml | 4 ++++ axum/Cargo.toml | 3 +++ axum/build.rs | 10 ++++++++++ axum/src/lib.rs | 2 +- build.rs | 10 ++++++++++ src/lib.rs | 32 ++++++++++++++++---------------- 6 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 axum/build.rs create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index 3b4a3b9..90a90cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ json = ["dep:serde", "dep:serde_json"] p2 = ["dep:wasip2"] p3 = ["dep:wasip3"] +[build-dependencies] +cfg_aliases.workspace = true + [dependencies] anyhow.workspace = true async-task.workspace = true @@ -134,6 +137,7 @@ async-task = "4.7" axum = { version = "0.8.6", default-features = false } bytes = "1.10.1" cargo_metadata = "0.22" +cfg_aliases = "0.2" clap = { version = "4.5.26", features = ["derive"] } futures-core = "0.3.19" futures-lite = "1.12.0" diff --git a/axum/Cargo.toml b/axum/Cargo.toml index baa5da4..a27ce78 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -21,6 +21,9 @@ default = ["p2"] p2 = ["wstd/p2"] p3 = ["wstd/p3"] +[build-dependencies] +cfg_aliases.workspace = true + [dev-dependencies] anyhow.workspace = true futures-concurrency.workspace = true diff --git a/axum/build.rs b/axum/build.rs new file mode 100644 index 0000000..5a5f457 --- /dev/null +++ b/axum/build.rs @@ -0,0 +1,10 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + cfg_aliases! { + // TODO https://github.com/bytecodealliance/wstd/issues/147: Swap these + // to use `target_env` instead. + p2: { feature = "p2" }, + p3: { feature = "p3" }, + } +} diff --git a/axum/src/lib.rs b/axum/src/lib.rs index 9536c89..c730fd2 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "p2")] +#![cfg(p2)] //! Support for the [`axum`] web server framework in wasi-http components, via //! [`wstd`]. //! diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5a5f457 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + cfg_aliases! { + // TODO https://github.com/bytecodealliance/wstd/issues/147: Swap these + // to use `target_env` instead. + p2: { feature = "p2" }, + p3: { feature = "p3" }, + } +} diff --git a/src/lib.rs b/src/lib.rs index 0dc8ed7..64ac672 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ //! **TCP echo server** //! //! ```rust,no_run -#![cfg_attr(feature = "p2", doc = include_str!("../examples/tcp_echo_server.rs"))] +#![cfg_attr(p2, doc = include_str!("../examples/tcp_echo_server.rs"))] //! ``` //! //! **HTTP Client** @@ -30,7 +30,7 @@ //! **HTTP Server** //! //! ```rust,no_run -#![cfg_attr(feature = "p2", doc = include_str!("../examples/http_server.rs"))] +#![cfg_attr(p2, doc = include_str!("../examples/http_server.rs"))] //! ``` //! //! # Design Decisions @@ -56,33 +56,33 @@ //! is specific to that are exposed from here. // Exactly one WASI backend must be selected. See the `p2`/`p3` features. -#[cfg(all(feature = "p2", feature = "p3"))] +#[cfg(all(p2, p3))] compile_error!( "the `p2` and `p3` features are mutually exclusive — enable exactly one WASI backend" ); -#[cfg(not(any(feature = "p2", feature = "p3")))] +#[cfg(not(any(p2, p3)))] compile_error!("exactly one of the `p2` or `p3` features must be enabled"); -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod future; -#[cfg(feature = "p2")] +#[cfg(p2)] #[macro_use] pub mod http; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod io; pub mod iter; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod net; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod rand; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod runtime; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod task; -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod time; -#[cfg(feature = "p2")] +#[cfg(p2)] pub use wstd_macro::{ attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, }; @@ -94,13 +94,13 @@ pub use wstd_macro::{ // private. #[doc(hidden)] pub mod __internal { - #[cfg(feature = "p2")] + #[cfg(p2)] pub use wasip2; - #[cfg(feature = "p3")] + #[cfg(p3)] pub use wasip3; } -#[cfg(feature = "p2")] +#[cfg(p2)] pub mod prelude { pub use crate::future::FutureExt as _; pub use crate::io::AsyncRead as _; From 0487755bbbe73dc9bf0ce5ac6dada4d3d3175eb1 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 2 Sep 2026 14:23:48 +0000 Subject: [PATCH 04/19] Require `&mut` for Streams --- examples/tcp_echo_server.rs | 6 ++- src/http/body.rs | 10 ++--- src/io/read.rs | 4 +- src/io/stdio.rs | 8 ++-- src/io/streams.rs | 35 ++++++++--------- src/io/write.rs | 4 +- src/net/tcp_listener.rs | 4 +- src/net/tcp_stream.rs | 77 ++++++++++++++++--------------------- 8 files changed, 68 insertions(+), 80 deletions(-) diff --git a/examples/tcp_echo_server.rs b/examples/tcp_echo_server.rs index f1dd895..3b02009 100644 --- a/examples/tcp_echo_server.rs +++ b/examples/tcp_echo_server.rs @@ -4,7 +4,7 @@ use wstd::net::TcpListener; #[wstd::main] async fn main() -> io::Result<()> { - let listener = TcpListener::bind("127.0.0.1:8080").await?; + let mut listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Listening on {}", listener.local_addr()?); println!("type `nc localhost 8080` to create a TCP client"); @@ -14,7 +14,9 @@ async fn main() -> io::Result<()> { println!("Accepted from: {}", stream.peer_addr()?); wstd::runtime::spawn(async move { // If echo copy fails, we can ignore it. - let _ = io::copy(&stream, &stream).await; + let mut stream = stream; + let (mut read_half, mut write_half) = stream.split(); + let _ = io::copy(&mut read_half, &mut write_half).await; }) .detach(); } diff --git a/src/http/body.rs b/src/http/body.rs index 95e8a2e..e04e624 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -77,7 +77,7 @@ impl Body { match self.0 { BodyInner::Incoming(incoming) => incoming.send(outgoing_body).await, BodyInner::Boxed(box_body) => { - let out_stream = AsyncOutputStream::new( + let mut out_stream = AsyncOutputStream::new( outgoing_body .write() .expect("outgoing body already written"), @@ -108,7 +108,7 @@ impl Body { } } BodyInner::Complete { data, trailers } => { - let out_stream = AsyncOutputStream::new( + let mut out_stream = AsyncOutputStream::new( outgoing_body .write() .expect("outgoing body already written"), @@ -348,14 +348,14 @@ impl Incoming { } async fn send(self, outgoing_body: WasiOutgoingBody) -> Result<(), Error> { let in_body = self.body; - let in_stream = + let mut in_stream = AsyncInputStream::new(in_body.stream().expect("incoming body already read")); - let out_stream = AsyncOutputStream::new( + let mut out_stream = AsyncOutputStream::new( outgoing_body .write() .expect("outgoing body already written"), ); - in_stream.copy_to(&out_stream).await.map_err(|e| { + in_stream.copy_to(&mut out_stream).await.map_err(|e| { Error::from(e).context("copying incoming body stream to outgoing body stream") })?; drop(in_stream); diff --git a/src/io/read.rs b/src/io/read.rs index a6a95da..474188e 100644 --- a/src/io/read.rs +++ b/src/io/read.rs @@ -28,7 +28,7 @@ pub trait AsyncRead { // If the `AsyncRead` implementation is an unbuffered wrapper around an // `AsyncInputStream`, some I/O operations can be more efficient. #[inline] - fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { + fn as_async_input_stream(&mut self) -> Option<&mut io::AsyncInputStream> { None } } @@ -45,7 +45,7 @@ impl AsyncRead for &mut R { } #[inline] - fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { + fn as_async_input_stream(&mut self) -> Option<&mut io::AsyncInputStream> { (**self).as_async_input_stream() } } diff --git a/src/io/stdio.rs b/src/io/stdio.rs index b2ac153..e403986 100644 --- a/src/io/stdio.rs +++ b/src/io/stdio.rs @@ -43,8 +43,8 @@ impl AsyncRead for Stdin { } #[inline] - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - Some(&self.stream) + fn as_async_input_stream(&mut self) -> Option<&mut AsyncInputStream> { + Some(&mut self.stream) } } @@ -93,7 +93,7 @@ impl AsyncWrite for Stdout { } #[inline] - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { self.stream.as_async_output_stream() } } @@ -143,7 +143,7 @@ impl AsyncWrite for Stderr { } #[inline] - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { self.stream.as_async_output_stream() } } diff --git a/src/io/streams.rs b/src/io/streams.rs index 3676d21..8644d66 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -26,7 +26,7 @@ impl AsyncInputStream { stream, } } - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<()> { + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<()> { // Lazily initialize the AsyncPollable let subscription = self .subscription @@ -43,12 +43,11 @@ impl AsyncInputStream { } } /// Await for read readiness. - async fn ready(&self) { + async fn ready(&mut self) { poll_fn(|cx| self.poll_ready(cx)).await } /// Asynchronously read from the input stream. - /// This method is the same as [`AsyncRead::read`], but doesn't require a `&mut self`. - pub async fn read(&self, buf: &mut [u8]) -> std::io::Result { + pub async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read = loop { self.ready().await; // Ideally, the ABI would be able to read directly into buf. @@ -76,7 +75,7 @@ impl AsyncInputStream { /// Move the entire contents of an input stream directly into an output /// stream, until the input stream has closed. This operation is optimized /// to avoid copying stream contents into and out of memory. - pub async fn copy_to(&self, writer: &AsyncOutputStream) -> std::io::Result { + pub async fn copy_to(&mut self, writer: &mut AsyncOutputStream) -> std::io::Result { let mut written = 0; loop { self.ready().await; @@ -128,7 +127,7 @@ impl AsyncRead for AsyncInputStream { } #[inline] - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + fn as_async_input_stream(&mut self) -> Option<&mut AsyncInputStream> { Some(self) } } @@ -150,9 +149,10 @@ impl AsyncInputChunkStream { impl futures_lite::stream::Stream for AsyncInputChunkStream { type Item = Result, std::io::Error>; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.stream.poll_ready(cx) { + let this = self.get_mut(); + match this.stream.poll_ready(cx) { Poll::Pending => Poll::Pending, - Poll::Ready(()) => match self.stream.stream.read(self.chunk_size as u64) { + Poll::Ready(()) => match this.stream.stream.read(this.chunk_size as u64) { Ok(r) if r.is_empty() => Poll::Pending, Ok(r) => Poll::Ready(Some(Ok(r))), Err(StreamError::LastOperationFailed(err)) => { @@ -233,7 +233,7 @@ impl AsyncOutputStream { } } /// Await write readiness. - async fn ready(&self) { + async fn ready(&mut self) { // Lazily initialize the AsyncPollable let subscription = self .subscription @@ -241,15 +241,14 @@ impl AsyncOutputStream { // Wait on readiness subscription.wait_for().await; } - /// Asynchronously write to the output stream. This method is the same as - /// [`AsyncWrite::write`], but doesn't require a `&mut self`. + /// Asynchronously write to the output stream. /// /// Awaits for write readiness, and then performs at most one write to the /// output stream. Returns how much of the argument `buf` was written, or /// a `std::io::Error` indicating either an error returned by the stream write /// using the debug string provided by the WASI error, or else that the, /// indicated by `std::io::ErrorKind::ConnectionReset`. - pub async fn write(&self, buf: &[u8]) -> std::io::Result { + pub async fn write(&mut self, buf: &[u8]) -> std::io::Result { // Loops at most twice. loop { match self.stream.check_write() { @@ -280,9 +279,8 @@ impl AsyncOutputStream { } } - /// Asynchronously write to the output stream. This method is the same as - /// [`AsyncWrite::write_all`], but doesn't require a `&mut self`. - pub async fn write_all(&self, buf: &[u8]) -> std::io::Result<()> { + /// Asynchronously write to the output stream. + pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { let mut to_write = &buf[0..]; loop { let bytes_written = self.write(to_write).await?; @@ -297,14 +295,11 @@ impl AsyncOutputStream { /// awaits until the flush is complete and the output stream is ready for /// writing again. /// - /// This method is the same as [`AsyncWrite::flush`], but doesn't require - /// a `&mut self`. - /// /// Fails with a `std::io::Error` indicating either an error returned by /// the stream flush, using the debug string provided by the WASI error, /// or else that the stream is closed, indicated by /// `std::io::ErrorKind::ConnectionReset`. - pub async fn flush(&self) -> std::io::Result<()> { + pub async fn flush(&mut self) -> std::io::Result<()> { match self.stream.flush() { Ok(()) => { self.ready().await; @@ -330,7 +325,7 @@ impl AsyncWrite for AsyncOutputStream { } #[inline] - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { Some(self) } } diff --git a/src/io/write.rs b/src/io/write.rs index 79cf0d9..12fd6ae 100644 --- a/src/io/write.rs +++ b/src/io/write.rs @@ -20,7 +20,7 @@ pub trait AsyncWrite { // If the `AsyncWrite` implementation is an unbuffered wrapper around an // `AsyncOutputStream`, some I/O operations can be more efficient. #[inline] - fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { + fn as_async_output_stream(&mut self) -> Option<&mut io::AsyncOutputStream> { None } } @@ -42,7 +42,7 @@ impl AsyncWrite for &mut W { } #[inline] - fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { + fn as_async_output_stream(&mut self) -> Option<&mut io::AsyncOutputStream> { (**self).as_async_output_stream() } } diff --git a/src/net/tcp_listener.rs b/src/net/tcp_listener.rs index 9a1f57a..b7af12b 100644 --- a/src/net/tcp_listener.rs +++ b/src/net/tcp_listener.rs @@ -56,7 +56,7 @@ impl TcpListener { } /// Returns an iterator over the connections being received on this listener. - pub fn incoming(&self) -> Incoming<'_> { + pub fn incoming(&mut self) -> Incoming<'_> { Incoming { listener: self } } } @@ -64,7 +64,7 @@ impl TcpListener { /// An iterator that infinitely accepts connections on a TcpListener. #[derive(Debug)] pub struct Incoming<'a> { - listener: &'a TcpListener, + listener: &'a mut TcpListener, } impl<'a> AsyncIterator for Incoming<'a> { diff --git a/src/net/tcp_stream.rs b/src/net/tcp_stream.rs index af3674a..977fb29 100644 --- a/src/net/tcp_stream.rs +++ b/src/net/tcp_stream.rs @@ -86,8 +86,17 @@ impl TcpStream { Ok(format!("{addr:?}")) } - pub fn split(&self) -> (ReadHalf<'_>, WriteHalf<'_>) { - (ReadHalf(self), WriteHalf(self)) + pub fn split(&mut self) -> (ReadHalf<'_>, WriteHalf<'_>) { + ( + ReadHalf { + stream: &mut self.input, + socket: &self.socket, + }, + WriteHalf { + stream: &mut self.output, + socket: &self.socket, + }, + ) } } @@ -104,18 +113,8 @@ impl io::AsyncRead for TcpStream { self.input.read(buf).await } - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - Some(&self.input) - } -} - -impl io::AsyncRead for &TcpStream { - async fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.input.read(buf).await - } - - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - (**self).as_async_input_stream() + fn as_async_input_stream(&mut self) -> Option<&mut AsyncInputStream> { + Some(&mut self.input) } } @@ -128,64 +127,56 @@ impl io::AsyncWrite for TcpStream { self.output.flush().await } - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - Some(&self.output) + fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { + Some(&mut self.output) } } -impl io::AsyncWrite for &TcpStream { - async fn write(&mut self, buf: &[u8]) -> io::Result { - self.output.write(buf).await - } - - async fn flush(&mut self) -> io::Result<()> { - self.output.flush().await - } +pub struct ReadHalf<'a> { + stream: &'a mut AsyncInputStream, + socket: &'a TcpSocket, +} - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - (**self).as_async_output_stream() +impl<'a> Drop for ReadHalf<'a> { + fn drop(&mut self) { + let _ = self + .socket + .shutdown(wasip2::sockets::tcp::ShutdownType::Receive); } } -pub struct ReadHalf<'a>(&'a TcpStream); impl<'a> io::AsyncRead for ReadHalf<'a> { async fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.0.read(buf).await + self.stream.read(buf).await } - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - self.0.as_async_input_stream() + fn as_async_input_stream(&mut self) -> Option<&mut AsyncInputStream> { + self.stream.as_async_input_stream() } } -impl<'a> Drop for ReadHalf<'a> { - fn drop(&mut self) { - let _ = self - .0 - .socket - .shutdown(wasip2::sockets::tcp::ShutdownType::Receive); - } +pub struct WriteHalf<'a> { + stream: &'a mut AsyncOutputStream, + socket: &'a TcpSocket, } -pub struct WriteHalf<'a>(&'a TcpStream); impl<'a> io::AsyncWrite for WriteHalf<'a> { async fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.write(buf).await + self.stream.write(buf).await } async fn flush(&mut self) -> io::Result<()> { - self.0.flush().await + self.stream.flush().await } - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - self.0.as_async_output_stream() + fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { + self.stream.as_async_output_stream() } } impl<'a> Drop for WriteHalf<'a> { fn drop(&mut self) { let _ = self - .0 .socket .shutdown(wasip2::sockets::tcp::ShutdownType::Send); } From 443ef72ef26593fd0da98c1bcfe0424eb72768ae Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 2 Sep 2026 15:09:22 +0000 Subject: [PATCH 05/19] remove &self stream methods --- src/http/body.rs | 2 +- src/io/streams.rs | 95 ++++++++++++++--------------------------------- 2 files changed, 29 insertions(+), 68 deletions(-) diff --git a/src/http/body.rs b/src/http/body.rs index e04e624..b4de743 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -3,7 +3,7 @@ use crate::http::{ error::Context as _, fields::{header_map_from_wasi, header_map_to_wasi}, }; -use crate::io::{AsyncInputStream, AsyncOutputStream}; +use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncWrite}; use crate::runtime::{AsyncPollable, Reactor, WaitFor}; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; diff --git a/src/io/streams.rs b/src/io/streams.rs index 8644d66..34fd3b0 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -46,32 +46,6 @@ impl AsyncInputStream { async fn ready(&mut self) { poll_fn(|cx| self.poll_ready(cx)).await } - /// Asynchronously read from the input stream. - pub async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let read = loop { - self.ready().await; - // Ideally, the ABI would be able to read directly into buf. - // However, with the default generated bindings, it returns a - // newly allocated vec, which we need to copy into buf. - match self.stream.read(buf.len() as u64) { - // A read of 0 bytes from WASI's `read` doesn't mean - // end-of-stream as it does in Rust. However, `self.ready()` - // cannot guarantee that at least one byte is ready for - // reading, so in this case we try again. - Ok(r) if r.is_empty() => continue, - Ok(r) => break r, - // 0 bytes from Rust's `read` means end-of-stream. - Err(StreamError::Closed) => return Ok(0), - Err(StreamError::LastOperationFailed(err)) => { - return Err(std::io::Error::other(err.to_debug_string())); - } - } - }; - let len = read.len(); - buf[0..len].copy_from_slice(&read); - Ok(len) - } - /// Move the entire contents of an input stream directly into an output /// stream, until the input stream has closed. This operation is optimized /// to avoid copying stream contents into and out of memory. @@ -123,7 +97,28 @@ impl AsyncInputStream { impl AsyncRead for AsyncInputStream { async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - Self::read(self, buf).await + let read = loop { + self.ready().await; + // Ideally, the ABI would be able to read directly into buf. + // However, with the default generated bindings, it returns a + // newly allocated vec, which we need to copy into buf. + match self.stream.read(buf.len() as u64) { + // A read of 0 bytes from WASI's `read` doesn't mean + // end-of-stream as it does in Rust. However, `self.ready()` + // cannot guarantee that at least one byte is ready for + // reading, so in this case we try again. + Ok(r) if r.is_empty() => continue, + Ok(r) => break r, + // 0 bytes from Rust's `read` means end-of-stream. + Err(StreamError::Closed) => return Ok(0), + Err(StreamError::LastOperationFailed(err)) => { + return Err(std::io::Error::other(err.to_debug_string())); + } + } + }; + let len = read.len(); + buf[0..len].copy_from_slice(&read); + Ok(len) } #[inline] @@ -241,14 +236,11 @@ impl AsyncOutputStream { // Wait on readiness subscription.wait_for().await; } - /// Asynchronously write to the output stream. - /// - /// Awaits for write readiness, and then performs at most one write to the - /// output stream. Returns how much of the argument `buf` was written, or - /// a `std::io::Error` indicating either an error returned by the stream write - /// using the debug string provided by the WASI error, or else that the, - /// indicated by `std::io::ErrorKind::ConnectionReset`. - pub async fn write(&mut self, buf: &[u8]) -> std::io::Result { +} + +impl AsyncWrite for AsyncOutputStream { + // Required methods + async fn write(&mut self, buf: &[u8]) -> std::io::Result { // Loops at most twice. loop { match self.stream.check_write() { @@ -278,28 +270,7 @@ impl AsyncOutputStream { } } } - - /// Asynchronously write to the output stream. - pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { - let mut to_write = &buf[0..]; - loop { - let bytes_written = self.write(to_write).await?; - to_write = &to_write[bytes_written..]; - if to_write.is_empty() { - return Ok(()); - } - } - } - - /// Asyncronously flush the output stream. Initiates a flush, and then - /// awaits until the flush is complete and the output stream is ready for - /// writing again. - /// - /// Fails with a `std::io::Error` indicating either an error returned by - /// the stream flush, using the debug string provided by the WASI error, - /// or else that the stream is closed, indicated by - /// `std::io::ErrorKind::ConnectionReset`. - pub async fn flush(&mut self) -> std::io::Result<()> { + async fn flush(&mut self) -> std::io::Result<()> { match self.stream.flush() { Ok(()) => { self.ready().await; @@ -313,16 +284,6 @@ impl AsyncOutputStream { } } } -} - -impl AsyncWrite for AsyncOutputStream { - // Required methods - async fn write(&mut self, buf: &[u8]) -> std::io::Result { - Self::write(self, buf).await - } - async fn flush(&mut self) -> std::io::Result<()> { - Self::flush(self).await - } #[inline] fn as_async_output_stream(&mut self) -> Option<&mut AsyncOutputStream> { From cabda1fe4ce3b74a2e9a6048f1931061cc1eca6f Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 2 Sep 2026 16:51:13 +0000 Subject: [PATCH 06/19] Add `wstd::main` and `wstd::test` macros for WASIp3 --- .cargo/config.toml | 2 +- macro/src/lib.rs | 152 +++++++++++++++++++++++++++++++ src/lib.rs | 3 + tests/http_first_byte_timeout.rs | 2 +- tests/macro_main.rs | 6 ++ tests/macro_test.rs | 7 ++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 tests/macro_main.rs create mode 100644 tests/macro_test.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index e9a242c..b5fc694 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,4 +2,4 @@ # wasmtime is given: # * AWS auth environment variables, for running the wstd-aws integration tests. # * . directory is available at . -runner = "wasmtime run -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." +runner = "wasmtime run -Sp3 -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 6498377..c787df7 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -2,6 +2,15 @@ use proc_macro::TokenStream; use quote::{quote, quote_spanned}; use syn::{ItemFn, parse_macro_input, spanned::Spanned}; +fn returns_unit(output: &syn::ReturnType) -> bool { + match output { + syn::ReturnType::Default => true, + syn::ReturnType::Type(_, ty) => { + matches!(&**ty, syn::Type::Tuple(tuple) if tuple.elems.is_empty()) + } + } +} + #[proc_macro_attribute] pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemFn); @@ -84,6 +93,149 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { .into() } +#[proc_macro_attribute] +pub fn attr_macro_main_p3(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + if input.sig.asyncness.is_none() { + return quote_spanned! { input.sig.fn_token.span()=> + compile_error!("fn must be `async fn`"); + } + .into(); + } + + if input.sig.ident != "main" { + return quote_spanned! { input.sig.ident.span()=> + compile_error!("only `async fn main` can be used for #[wstd::main]"); + } + .into(); + } + + if !input.sig.inputs.is_empty() { + return quote_spanned! { input.sig.inputs.span()=> + compile_error!("arguments to main are not supported"); + } + .into(); + } + let attrs = input.attrs; + let output = input.sig.output; + let block = input.block; + let run_result = if returns_unit(&output) { + quote! { + __run().await; + ::core::result::Result::Ok(()) + } + } else { + quote! { + ::core::result::Result::map_err(__run().await, |_| ()) + } + }; + quote! { + struct __WstdCliRunner; + + impl ::wstd::__internal::wasip3::exports::cli::run::Guest for __WstdCliRunner { + async fn run() -> ::core::result::Result<(), ()> { + #(#attrs)* + async fn __run() #output { + #block + } + + #run_result + } + } + + ::wstd::__internal::wasip3::cli::command::export!( + __WstdCliRunner with_types_in ::wstd::__internal::wasip3 + ); + + // Provide a `main` so users don't have to write `#![no_main]`. + fn main() { + ::core::unreachable!( + "wstd p3 components run via the wasi:cli/run@0.3.0 export, not `main`" + ) + } + } + .into() +} + +#[proc_macro_attribute] +pub fn attr_macro_test_p3(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + if input.sig.asyncness.is_none() { + return quote_spanned! { input.sig.fn_token.span()=> + compile_error!("fn must be `async fn`"); + } + .into(); + } + + if !input.sig.inputs.is_empty() { + return quote_spanned! { input.sig.inputs.span()=> + compile_error!("arguments to a test are not supported"); + } + .into(); + } + let attrs = input.attrs; + let output = input.sig.output; + let block = input.block; + let name = input.sig.ident; + let message = format!("test {name} ... "); + let run_result = if returns_unit(&output) { + quote! { + ::std::println!(""); + ::std::print!(#message); + __run().await; + ::std::println!("ok"); + ::std::println!(""); + ::core::result::Result::Ok(()) + } + } else { + quote! { + ::std::println!(""); + ::std::print!(#message); + match __run().await { + ::core::result::Result::Ok(_) => { + ::std::println!("ok"); + ::std::println!(""); + ::core::result::Result::Ok(()) + } + ::core::result::Result::Err(err) => { + ::std::println!("failed"); + ::std::println!("Error {:?}", err); + ::std::println!(""); + ::core::result::Result::Err(()) + } + } + } + }; + quote! { + struct __WstdCliRunner; + + impl ::wstd::__internal::wasip3::exports::cli::run::Guest for __WstdCliRunner { + async fn run() -> ::core::result::Result<(), ()> { + #(#attrs)* + async fn __run() #output { + #block + } + + #run_result + } + } + + ::wstd::__internal::wasip3::cli::command::export!( + __WstdCliRunner with_types_in ::wstd::__internal::wasip3 + ); + + // Provide a `main` so users don't have to write `#![no_main]`. + fn main() { + ::core::unreachable!( + "wstd p3 components run via the wasi:cli/run@0.3.0 export, not `main`" + ) + } + } + .into() +} + /// Enables a HTTP server main function, for creating [HTTP servers]. /// /// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html diff --git a/src/lib.rs b/src/lib.rs index 64ac672..66a6184 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,9 @@ pub use wstd_macro::{ attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, }; +#[cfg(p3)] +pub use wstd_macro::{attr_macro_main_p3 as main, attr_macro_test_p3 as test}; + // Re-export the active WASI backend crate for use only by `wstd-macro` macros. // The proc macros need to generate code that uses these definitions, but we // don't want to treat it as part of our public API with regards to semver, so diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index 5fd47be..a77b9fb 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,6 +1,6 @@ use wstd::http::{Body, Client, Request, error::ErrorCode}; -#[wstd::main] +#[wstd::test] async fn main() -> Result<(), Box> { // Set first byte timeout to 1/2 second. let mut client = Client::new(); diff --git a/tests/macro_main.rs b/tests/macro_main.rs new file mode 100644 index 0000000..044bab2 --- /dev/null +++ b/tests/macro_main.rs @@ -0,0 +1,6 @@ +// Verifies that the `main` macro is compiling. + +#[wstd::main] +async fn main() { + assert_eq!(1 + 1, 2); +} diff --git a/tests/macro_test.rs b/tests/macro_test.rs new file mode 100644 index 0000000..eb9c936 --- /dev/null +++ b/tests/macro_test.rs @@ -0,0 +1,7 @@ +// Verify that the test macro is acutally running. + +#[wstd::test] +async fn pure_computation() -> Result<(), String> { + assert_eq!(1 + 1, 2); + Ok(()) +} From adb35fcb17d5d6620bf34464b4909c3b4aeacb9c Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 00:49:00 +0000 Subject: [PATCH 07/19] switch from feature to target --- .cargo/config.toml | 6 ++ .github/actions/install-rust/action.yml | 5 ++ .github/workflows/ci.yaml | 8 +-- Cargo.toml | 75 +++---------------------- axum/Cargo.toml | 20 ------- axum/build.rs | 10 ---- axum/examples/hello_world.rs | 3 + axum/examples/hello_world_nomacro.rs | 3 + axum/examples/weather.rs | 3 + axum/src/lib.rs | 2 +- build.rs | 10 ---- examples/complex_http_client.rs | 3 + examples/http_client.rs | 3 + examples/http_server.rs | 3 + examples/http_server_proxy.rs | 3 + examples/tcp_echo_server.rs | 3 + examples/tcp_stream_client.rs | 3 + src/lib.rs | 36 +++++------- tests/http_first_byte_timeout.rs | 2 + tests/http_get.rs | 2 + tests/http_get_json.rs | 2 + tests/http_handle_error_code.rs | 2 + tests/http_post.rs | 2 + tests/http_post_json.rs | 2 + tests/http_timeout.rs | 2 + tests/sleep.rs | 2 + 26 files changed, 80 insertions(+), 135 deletions(-) delete mode 100644 axum/build.rs delete mode 100644 build.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index e9a242c..aa3925b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,9 @@ # * AWS auth environment variables, for running the wstd-aws integration tests. # * . directory is available at . runner = "wasmtime run -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." + +[target.wasm32-wasip3] +# wasmtime is given: +# * AWS auth environment variables, for running the wstd-aws integration tests. +# * . directory is available at . +runner = "wasmtime run -Shttp -Sp3 --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml index 0d11907..57cc11c 100644 --- a/.github/actions/install-rust/action.yml +++ b/.github/actions/install-rust/action.yml @@ -31,6 +31,11 @@ runs: rustup update "${{ steps.select.outputs.version }}" --no-self-update rustup default "${{ steps.select.outputs.version }}" + # Required for -Zbuild-std for the wasip3 target. + if [ "${{ inputs.toolchain }}" = "nightly" ]; then + rustup component add rust-src + fi + # Save disk space by avoiding incremental compilation. Also turn down # debuginfo from 2 to 0 to help save disk space. cat >> "$GITHUB_ENV" < Date: Wed, 9 Sep 2026 13:03:53 +0000 Subject: [PATCH 08/19] merge fixes --- examples/udp_echo_server.rs | 3 +++ examples/udp_stream_client.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/examples/udp_echo_server.rs b/examples/udp_echo_server.rs index f840107..7c10bc3 100644 --- a/examples/udp_echo_server.rs +++ b/examples/udp_echo_server.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(target_env = "p2"), no_main)] +#![cfg(target_env = "p2")] + use wstd::io; use wstd::net::UdpSocket; diff --git a/examples/udp_stream_client.rs b/examples/udp_stream_client.rs index 013caee..68626d6 100644 --- a/examples/udp_stream_client.rs +++ b/examples/udp_stream_client.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(target_env = "p2"), no_main)] +#![cfg(target_env = "p2")] + use wstd::io; use wstd::net::{UdpSocket, UdpStream}; From 6435d57d3330a76224630a92c6aeff371354f255 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:12:01 +0000 Subject: [PATCH 09/19] add job printing rust version --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9e2f537..46cbec8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,6 +48,9 @@ jobs: role-to-assume: arn:aws:iam::313377415443:role/wstd-aws-ci-role role-session-name: github-ci + - name: rust version + run: cargo --version + - name: check run: cargo check --workspace --all --bins --examples From 663c532863e5734eadfc2051f9b712c2d0de3062 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:24:11 +0000 Subject: [PATCH 10/19] try override instead of default --- .github/actions/install-rust/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml index 57cc11c..c39c4be 100644 --- a/.github/actions/install-rust/action.yml +++ b/.github/actions/install-rust/action.yml @@ -29,7 +29,9 @@ runs: run: | rustup set profile minimal rustup update "${{ steps.select.outputs.version }}" --no-self-update - rustup default "${{ steps.select.outputs.version }}" + # `rustup default` gets overwritten by rust-toolchain.toml, `rustup + # override` has higher precedence though. + rustup override set "${{ steps.select.outputs.version }}" # Required for -Zbuild-std for the wasip3 target. if [ "${{ inputs.toolchain }}" = "nightly" ]; then From d306e06e369563de71ea16cfae143ff3944700bf Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:26:41 +0000 Subject: [PATCH 11/19] explicitly add wasi targets --- .github/actions/install-rust/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml index c39c4be..18957d4 100644 --- a/.github/actions/install-rust/action.yml +++ b/.github/actions/install-rust/action.yml @@ -33,8 +33,11 @@ runs: # override` has higher precedence though. rustup override set "${{ steps.select.outputs.version }}" + rustup target add wasm32-wasip2 + # Required for -Zbuild-std for the wasip3 target. if [ "${{ inputs.toolchain }}" = "nightly" ]; then + rustup target add wasm32-wasip3 rustup component add rust-src fi From 2f8c1c2c449946dbb7f6c710ec784a0d7c76cbf8 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:32:48 +0000 Subject: [PATCH 12/19] bump msrv to latest version with p3 target_env --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e990b9e..9b903c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ license = "Apache-2.0 WITH LLVM-exception" repository = "https://github.com/bytecodealliance/wstd" keywords = ["WebAssembly", "async", "stdlib", "Components"] categories = ["wasm", "asynchronous"] -rust-version = "1.91.1" +rust-version = "1.92.0" authors = [ "Yoshua Wuyts ", "Pat Hickey ", From 5841233cbe314258dfa55c836c32fd517e23f084 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:38:36 +0000 Subject: [PATCH 13/19] include target_os in directives --- Cargo.toml | 4 ++-- axum/examples/hello_world.rs | 4 ++-- axum/examples/hello_world_nomacro.rs | 4 ++-- axum/examples/weather.rs | 4 ++-- axum/src/lib.rs | 2 +- examples/complex_http_client.rs | 4 ++-- examples/http_client.rs | 4 ++-- examples/http_server.rs | 4 ++-- examples/http_server_proxy.rs | 4 ++-- examples/tcp_echo_server.rs | 4 ++-- examples/tcp_stream_client.rs | 4 ++-- examples/udp_echo_server.rs | 4 ++-- examples/udp_stream_client.rs | 4 ++-- src/lib.rs | 24 ++++++++++++------------ tests/http_first_byte_timeout.rs | 2 +- tests/http_get.rs | 2 +- tests/http_get_json.rs | 2 +- tests/http_handle_error_code.rs | 2 +- tests/http_post.rs | 2 +- tests/http_post_json.rs | 2 +- tests/http_timeout.rs | 2 +- tests/sleep.rs | 2 +- 22 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b903c1..2a7e28b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,10 +33,10 @@ wstd-macro.workspace = true serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } -[target.'cfg(target_env = "p2")'.dependencies] +[target.'cfg(all(target_os = "wasi", target_env = "p2"))'.dependencies] wasip2.workspace = true -[target.'cfg(target_env = "p3")'.dependencies] +[target.'cfg(all(target_os = "wasi", target_env = "p3"))'.dependencies] wasip3.workspace = true [dev-dependencies] diff --git a/axum/examples/hello_world.rs b/axum/examples/hello_world.rs index 8cf124c..9931344 100644 --- a/axum/examples/hello_world.rs +++ b/axum/examples/hello_world.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] //! Run with //! diff --git a/axum/examples/hello_world_nomacro.rs b/axum/examples/hello_world_nomacro.rs index 44744e5..bc0a39b 100644 --- a/axum/examples/hello_world_nomacro.rs +++ b/axum/examples/hello_world_nomacro.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] //! Run with //! diff --git a/axum/examples/weather.rs b/axum/examples/weather.rs index f404c0f..0df8f97 100644 --- a/axum/examples/weather.rs +++ b/axum/examples/weather.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] //! This demo app shows a Axum based wasi-http server making an arbitrary //! number of http requests as part of serving a single response. diff --git a/axum/src/lib.rs b/axum/src/lib.rs index d113410..c5a2af1 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] //! Support for the [`axum`] web server framework in wasi-http components, via //! [`wstd`]. //! diff --git a/examples/complex_http_client.rs b/examples/complex_http_client.rs index 7dadebf..7c3e470 100644 --- a/examples/complex_http_client.rs +++ b/examples/complex_http_client.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use anyhow::{Result, anyhow}; use clap::{ArgAction, Parser}; diff --git a/examples/http_client.rs b/examples/http_client.rs index 6850d60..178fbeb 100644 --- a/examples/http_client.rs +++ b/examples/http_client.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use anyhow::{Result, anyhow}; use clap::{ArgAction, Parser}; diff --git a/examples/http_server.rs b/examples/http_server.rs index 5486f0c..fa67518 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use anyhow::{Context, Result}; use futures_lite::stream::{once_future, unfold}; diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 77bb231..8d0cf01 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] //! Run the example with: //! ```sh diff --git a/examples/tcp_echo_server.rs b/examples/tcp_echo_server.rs index 3e8aef2..319f911 100644 --- a/examples/tcp_echo_server.rs +++ b/examples/tcp_echo_server.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::io; use wstd::iter::AsyncIterator; diff --git a/examples/tcp_stream_client.rs b/examples/tcp_stream_client.rs index c14b267..a269b8c 100644 --- a/examples/tcp_stream_client.rs +++ b/examples/tcp_stream_client.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::io::{self, AsyncRead, AsyncWrite}; use wstd::net::TcpStream; diff --git a/examples/udp_echo_server.rs b/examples/udp_echo_server.rs index 7c10bc3..c441d87 100644 --- a/examples/udp_echo_server.rs +++ b/examples/udp_echo_server.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::io; use wstd::net::UdpSocket; diff --git a/examples/udp_stream_client.rs b/examples/udp_stream_client.rs index 68626d6..f26f5d5 100644 --- a/examples/udp_stream_client.rs +++ b/examples/udp_stream_client.rs @@ -1,5 +1,5 @@ -#![cfg_attr(not(target_env = "p2"), no_main)] -#![cfg(target_env = "p2")] +#![cfg_attr(not(all(target_os = "wasi", target_env = "p2")), no_main)] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::io; use wstd::net::{UdpSocket, UdpStream}; diff --git a/src/lib.rs b/src/lib.rs index c3442aa..e7abd6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,26 +55,26 @@ //! These are unique capabilities provided by WASI 0.2, and because this library //! is specific to that are exposed from here. -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod future; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] #[macro_use] pub mod http; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod io; pub mod iter; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod net; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod rand; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod runtime; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod task; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod time; -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub use wstd_macro::{ attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, }; @@ -86,13 +86,13 @@ pub use wstd_macro::{ // private. #[doc(hidden)] pub mod __internal { - #[cfg(target_env = "p2")] + #[cfg(all(target_os = "wasi", target_env = "p2"))] pub use wasip2; - #[cfg(target_env = "p3")] + #[cfg(all(target_os = "wasi", target_env = "p3"))] pub use wasip3; } -#[cfg(target_env = "p2")] +#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod prelude { pub use crate::future::FutureExt as _; pub use crate::io::AsyncRead as _; diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index fb22cd3..2162294 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::http::{Body, Client, Request, error::ErrorCode}; diff --git a/tests/http_get.rs b/tests/http_get.rs index 5660e0e..1735d22 100644 --- a/tests/http_get.rs +++ b/tests/http_get.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use std::error::Error; use wstd::http::{Body, Client, HeaderValue, Request}; diff --git a/tests/http_get_json.rs b/tests/http_get_json.rs index 5596e0b..641c2b7 100644 --- a/tests/http_get_json.rs +++ b/tests/http_get_json.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use serde::Deserialize; use std::error::Error; diff --git a/tests/http_handle_error_code.rs b/tests/http_handle_error_code.rs index 0bbdd1d..fa72e0c 100644 --- a/tests/http_handle_error_code.rs +++ b/tests/http_handle_error_code.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::http::{Body, Client, Request, error::ErrorCode}; diff --git a/tests/http_post.rs b/tests/http_post.rs index 522c47b..1082a40 100644 --- a/tests/http_post.rs +++ b/tests/http_post.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use std::error::Error; use wstd::http::{Client, HeaderValue, Request}; diff --git a/tests/http_post_json.rs b/tests/http_post_json.rs index 1dc07f2..f67d050 100644 --- a/tests/http_post_json.rs +++ b/tests/http_post_json.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use serde::{Deserialize, Serialize}; use std::error::Error; diff --git a/tests/http_timeout.rs b/tests/http_timeout.rs index 04f60fc..6a156d9 100644 --- a/tests/http_timeout.rs +++ b/tests/http_timeout.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use wstd::future::FutureExt; use wstd::http::{Body, Client, Request}; diff --git a/tests/sleep.rs b/tests/sleep.rs index ac8c72a..c888302 100644 --- a/tests/sleep.rs +++ b/tests/sleep.rs @@ -1,4 +1,4 @@ -#![cfg(target_env = "p2")] +#![cfg(all(target_os = "wasi", target_env = "p2"))] use std::error::Error; use wstd::task::sleep; From 114abd09ebcd3a7e8fcda85644ee32b4ee95588a Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 13:47:26 +0000 Subject: [PATCH 14/19] fixes from merge --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6f89c2a..d43bc97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub use wstd_macro::{ attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, }; -#[cfg(p3)] +#[cfg(all(target_os = "wasi", target_env = "p3"))] pub use wstd_macro::{attr_macro_main_p3 as main, attr_macro_test_p3 as test}; // Re-export the active WASI backend crate for use only by `wstd-macro` macros. From 9d58000ed409fe9fe0e60cc289d8bacd9ed1bc85 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 14:42:59 +0000 Subject: [PATCH 15/19] use wit_bindgen::block_on in macros --- {tests => examples}/macro_main.rs | 2 +- macro/src/lib.rs | 143 ------------------------------ src/lib.rs | 13 ++- tests/macro_test.rs | 7 +- 4 files changed, 16 insertions(+), 149 deletions(-) rename {tests => examples}/macro_main.rs (74%) diff --git a/tests/macro_main.rs b/examples/macro_main.rs similarity index 74% rename from tests/macro_main.rs rename to examples/macro_main.rs index 044bab2..86d50f6 100644 --- a/tests/macro_main.rs +++ b/examples/macro_main.rs @@ -2,5 +2,5 @@ #[wstd::main] async fn main() { - assert_eq!(1 + 1, 2); + println!("Hello world"); } diff --git a/macro/src/lib.rs b/macro/src/lib.rs index c787df7..c9c73b3 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -93,149 +93,6 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -#[proc_macro_attribute] -pub fn attr_macro_main_p3(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemFn); - - if input.sig.asyncness.is_none() { - return quote_spanned! { input.sig.fn_token.span()=> - compile_error!("fn must be `async fn`"); - } - .into(); - } - - if input.sig.ident != "main" { - return quote_spanned! { input.sig.ident.span()=> - compile_error!("only `async fn main` can be used for #[wstd::main]"); - } - .into(); - } - - if !input.sig.inputs.is_empty() { - return quote_spanned! { input.sig.inputs.span()=> - compile_error!("arguments to main are not supported"); - } - .into(); - } - let attrs = input.attrs; - let output = input.sig.output; - let block = input.block; - let run_result = if returns_unit(&output) { - quote! { - __run().await; - ::core::result::Result::Ok(()) - } - } else { - quote! { - ::core::result::Result::map_err(__run().await, |_| ()) - } - }; - quote! { - struct __WstdCliRunner; - - impl ::wstd::__internal::wasip3::exports::cli::run::Guest for __WstdCliRunner { - async fn run() -> ::core::result::Result<(), ()> { - #(#attrs)* - async fn __run() #output { - #block - } - - #run_result - } - } - - ::wstd::__internal::wasip3::cli::command::export!( - __WstdCliRunner with_types_in ::wstd::__internal::wasip3 - ); - - // Provide a `main` so users don't have to write `#![no_main]`. - fn main() { - ::core::unreachable!( - "wstd p3 components run via the wasi:cli/run@0.3.0 export, not `main`" - ) - } - } - .into() -} - -#[proc_macro_attribute] -pub fn attr_macro_test_p3(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemFn); - - if input.sig.asyncness.is_none() { - return quote_spanned! { input.sig.fn_token.span()=> - compile_error!("fn must be `async fn`"); - } - .into(); - } - - if !input.sig.inputs.is_empty() { - return quote_spanned! { input.sig.inputs.span()=> - compile_error!("arguments to a test are not supported"); - } - .into(); - } - let attrs = input.attrs; - let output = input.sig.output; - let block = input.block; - let name = input.sig.ident; - let message = format!("test {name} ... "); - let run_result = if returns_unit(&output) { - quote! { - ::std::println!(""); - ::std::print!(#message); - __run().await; - ::std::println!("ok"); - ::std::println!(""); - ::core::result::Result::Ok(()) - } - } else { - quote! { - ::std::println!(""); - ::std::print!(#message); - match __run().await { - ::core::result::Result::Ok(_) => { - ::std::println!("ok"); - ::std::println!(""); - ::core::result::Result::Ok(()) - } - ::core::result::Result::Err(err) => { - ::std::println!("failed"); - ::std::println!("Error {:?}", err); - ::std::println!(""); - ::core::result::Result::Err(()) - } - } - } - }; - quote! { - struct __WstdCliRunner; - - impl ::wstd::__internal::wasip3::exports::cli::run::Guest for __WstdCliRunner { - async fn run() -> ::core::result::Result<(), ()> { - #(#attrs)* - async fn __run() #output { - #block - } - - #run_result - } - } - - ::wstd::__internal::wasip3::cli::command::export!( - __WstdCliRunner with_types_in ::wstd::__internal::wasip3 - ); - - // Provide a `main` so users don't have to write `#![no_main]`. - fn main() { - ::core::unreachable!( - "wstd p3 components run via the wasi:cli/run@0.3.0 export, not `main`" - ) - } - } - .into() -} - /// Enables a HTTP server main function, for creating [HTTP servers]. /// /// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html diff --git a/src/lib.rs b/src/lib.rs index d43bc97..313bea5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,19 +69,24 @@ pub mod net; pub mod rand; #[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod runtime; +#[cfg(all(target_os = "wasi", target_env = "p3"))] +pub mod runtime { + pub fn block_on(fut: F) -> F::Output + where + F: Future + 'static, + { + wasip3::wit_bindgen::block_on(fut) + } +} #[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod task; #[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod time; -#[cfg(all(target_os = "wasi", target_env = "p2"))] pub use wstd_macro::{ attr_macro_http_server as http_server, attr_macro_main as main, attr_macro_test as test, }; -#[cfg(all(target_os = "wasi", target_env = "p3"))] -pub use wstd_macro::{attr_macro_main_p3 as main, attr_macro_test_p3 as test}; - // Re-export the active WASI backend crate for use only by `wstd-macro` macros. // The proc macros need to generate code that uses these definitions, but we // don't want to treat it as part of our public API with regards to semver, so diff --git a/tests/macro_test.rs b/tests/macro_test.rs index eb9c936..5dd0a22 100644 --- a/tests/macro_test.rs +++ b/tests/macro_test.rs @@ -1,7 +1,12 @@ // Verify that the test macro is acutally running. #[wstd::test] -async fn pure_computation() -> Result<(), String> { +async fn computation() -> Result<(), String> { assert_eq!(1 + 1, 2); Ok(()) } + +#[wstd::test] +async fn second_computation() { + assert_eq!(1 + 1, 2); +} From 14e1bceb47b0d122b5fb35c8fec342a54b9d1a23 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 14:44:57 +0000 Subject: [PATCH 16/19] remove build-std --- .github/actions/install-rust/action.yml | 2 -- .github/workflows/ci.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml index 18957d4..3c16f0c 100644 --- a/.github/actions/install-rust/action.yml +++ b/.github/actions/install-rust/action.yml @@ -35,10 +35,8 @@ runs: rustup target add wasm32-wasip2 - # Required for -Zbuild-std for the wasip3 target. if [ "${{ inputs.toolchain }}" = "nightly" ]; then rustup target add wasm32-wasip3 - rustup component add rust-src fi # Save disk space by avoiding incremental compilation. Also turn down diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46cbec8..598dd5f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -59,7 +59,7 @@ jobs: - name: wstd p3 tests if: matrix.rust == 'nightly' - run: cargo test -p wstd -p wstd-axum --target wasm32-wasip3 -Z build-std=std,panic_abort -- --nocapture + run: cargo test -p wstd -p wstd-axum --target wasm32-wasip3 -- --nocapture - name: test-programs tests run: cargo test -p test-programs -- --nocapture From 2adec5890d299a82c4d84b3406ec01c5417714d6 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 14:46:06 +0000 Subject: [PATCH 17/19] remove dead code --- macro/src/lib.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index c9c73b3..6498377 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -2,15 +2,6 @@ use proc_macro::TokenStream; use quote::{quote, quote_spanned}; use syn::{ItemFn, parse_macro_input, spanned::Spanned}; -fn returns_unit(output: &syn::ReturnType) -> bool { - match output { - syn::ReturnType::Default => true, - syn::ReturnType::Type(_, ty) => { - matches!(&**ty, syn::Type::Tuple(tuple) if tuple.elems.is_empty()) - } - } -} - #[proc_macro_attribute] pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemFn); From 6150b98cb87b8bed2839cc6d91a26315b8d8ddc2 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 14:52:36 +0000 Subject: [PATCH 18/19] no main for non-wasi targets --- examples/macro_main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/macro_main.rs b/examples/macro_main.rs index 86d50f6..d301547 100644 --- a/examples/macro_main.rs +++ b/examples/macro_main.rs @@ -1,4 +1,7 @@ -// Verifies that the `main` macro is compiling. +#![cfg_attr(not(target_os = "wasi"), no_main)] +#![cfg(target_os = "wasi")] + +//! Verifies that the `main` macro is compiling. #[wstd::main] async fn main() { From 64f20f5f8bd93b60ae86c80f4f6fff77209d25a4 Mon Sep 17 00:00:00 2001 From: Adam Bratschi-Kaye Date: Wed, 9 Sep 2026 17:51:04 +0000 Subject: [PATCH 19/19] change block_on static requirement --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 313bea5..dca9cd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,9 +71,10 @@ pub mod rand; pub mod runtime; #[cfg(all(target_os = "wasi", target_env = "p3"))] pub mod runtime { - pub fn block_on(fut: F) -> F::Output + pub fn block_on(fut: F) -> F::Output where - F: Future + 'static, + F: Future, + T: 'static, { wasip3::wit_bindgen::block_on(fut) }