From 2712b9358ce6228950c41e1c8389e3e6e3f26380 Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Tue, 7 Apr 2026 09:05:35 -0400 Subject: [PATCH 1/2] feat: add wasip3 behind flag --- .cargo/config.toml | 4 +- .github/workflows/ci.yaml | 5 +- Cargo.toml | 14 +- axum/Cargo.toml | 7 +- macro/src/lib.rs | 83 +--- src/http/body.rs | 198 +++++--- src/http/client.rs | 83 ++-- src/http/error.rs | 5 + src/http/fields.rs | 19 + src/http/method.rs | 4 + src/http/mod.rs | 135 ++++++ src/http/request.rs | 162 ++++++- src/http/response.rs | 46 +- src/http/scheme.rs | 4 + src/http/server.rs | 168 +++++-- src/io/copy.rs | 3 +- src/io/mod.rs | 1 + src/io/stdio.rs | 53 ++- src/io/streams.rs | 764 ++++++++++++++++++++----------- src/lib.rs | 9 +- src/net/mod.rs | 24 +- src/net/tcp_listener.rs | 338 +++++++++----- src/net/tcp_stream.rs | 419 +++++++++++------ src/rand/mod.rs | 4 + src/runtime/block_on.rs | 35 +- src/runtime/mod.rs | 12 +- src/runtime/reactor.rs | 923 +++++++++++++++++++++----------------- src/time/duration.rs | 5 + src/time/instant.rs | 12 +- src/time/mod.rs | 124 ++++- tests/sleep.rs | 17 + tests/stdio.rs | 18 + tests/timer.rs | 35 ++ 33 files changed, 2585 insertions(+), 1148 deletions(-) create mode 100644 tests/stdio.rs create mode 100644 tests/timer.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index e9a242c..a50e65d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,7 @@ [target.wasm32-wasip2] # wasmtime is given: +# * p3 enabled for wasip3 component support (backwards compat with p2) +# * http enabled for wasi-http tests # * 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 -Wcomponent-model-async -Wcomponent-model-async-builtins -Wcomponent-model-async-stackful -Sp3 -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1362609..b304e81 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,9 +51,12 @@ jobs: - name: check run: cargo check --workspace --all --bins --examples - - name: wstd tests + - name: wstd tests (wasip2) run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 -- --nocapture + - name: wstd tests (wasip3) + run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 --no-default-features --features wasip3 -- --nocapture + - name: test-programs tests run: cargo test -p test-programs -- --nocapture if: steps.creds.outcome == 'success' diff --git a/Cargo.toml b/Cargo.toml index 4e248eb..738b057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,10 @@ repository.workspace = true rust-version.workspace = true [features] -default = ["json"] +default = ["json", "wasip2"] json = ["dep:serde", "dep:serde_json"] +wasip2 = ["dep:wasip2"] +wasip3 = ["dep:wasip3", "dep:wit-bindgen"] [dependencies] anyhow.workspace = true @@ -27,7 +29,9 @@ http.workspace = true itoa.workspace = true pin-project-lite.workspace = true slab.workspace = true -wasip2.workspace = true +wasip2 = { workspace = true, optional = true } +wasip3 = { workspace = true, optional = true } +wit-bindgen = { workspace = true, optional = true } wstd-macro.workspace = true # optional @@ -95,7 +99,9 @@ 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.6" } +wasip3 = "0.5" +wit-bindgen = { version = "0.54", default-features = false, features = ["async", "async-spawn", "inter-task-wakeup"] } +wstd = { path = ".", version = "=0.6.6", default-features = false } wstd-axum = { path = "./axum", version = "=0.6.6" } wstd-axum-macro = { path = "./axum/macro", version = "=0.6.6" } wstd-macro = { path = "./macro", version = "=0.6.6" } @@ -103,5 +109,5 @@ wstd-macro = { path = "./macro", version = "=0.6.6" } [package.metadata.docs.rs] all-features = true targets = [ - "wasm32-wasip2" + "wasm32-wasip2", ] diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 154a021..a7430fc 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -10,10 +10,15 @@ categories.workspace = true repository.workspace = true rust-version.workspace = true +[features] +default = ["wasip2"] +wasip2 = ["wstd/wasip2"] +wasip3 = ["wstd/wasip3"] + [dependencies] axum.workspace = true tower-service.workspace = true -wstd.workspace = true +wstd = { workspace = true, default-features = false } wstd-axum-macro.workspace = true [dev-dependencies] diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 6498377..d1544c6 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -100,12 +100,6 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemFn); - let (run_async, run_await) = if input.sig.asyncness.is_some() { - (quote!(async), quote!(.await)) - } else { - (quote!(), quote!()) - }; - let output = &input.sig.output; let inputs = &input.sig.inputs; let name = &input.sig.ident; @@ -120,63 +114,32 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr .into(); } + // Delegate to wstd's conditionally-compiled declarative macro. + // The `cfg` checks in `__http_server_export!` run in wstd's context, + // so consumers don't need to define wasip2/wasip3 features themselves. + // + // We use @async/@sync markers so the declarative macro can construct + // the handler call in its own hygiene context (avoiding variable name + // mismatches between proc macro and declarative macro scopes) + // NOTE: we can avoid this once wasm32-wasip3 is an available target + let asyncness = if input.sig.asyncness.is_some() { + quote!(@async) + } else { + quote!(@sync) + }; + + let run_async = if input.sig.asyncness.is_some() { + quote!(async) + } else { + quote!() + }; + quote! { - struct TheServer; - - impl ::wstd::__internal::wasip2::exports::http::incoming_handler::Guest for TheServer { - fn handle( - request: ::wstd::__internal::wasip2::http::types::IncomingRequest, - response_out: ::wstd::__internal::wasip2::http::types::ResponseOutparam - ) { - #(#attrs)* - #vis #run_async fn __run(#inputs) #output { - #body - } - - let responder = ::wstd::http::server::Responder::new(response_out); - ::wstd::runtime::block_on(async move { - match ::wstd::http::request::try_from_incoming(request) { - Ok(request) => match __run(request) #run_await { - Ok(response) => { responder.respond(response).await.unwrap() }, - Err(err) => responder.fail(err), - } - Err(err) => responder.fail(err), - } - }) - } + ::wstd::__http_server_export! { + #asyncness + { #(#attrs)* #vis #run_async fn __run(#inputs) #output { #body } } } - ::wstd::__internal::wasip2::http::proxy::export!(TheServer with_types_in ::wstd::__internal::wasip2); - - // Provide an actual function named `main`. - // - // WASI HTTP server components don't use a traditional `main` function. - // They export a function named `handle` which takes a `Request` - // argument, and which may be called multiple times on the same - // instance. To let users write a familiar `fn main` in a file - // named src/main.rs, we provide this `wstd::http_server` macro, which - // transforms the user's `fn main` into the appropriate `handle` - // function. - // - // However, when the top-level file is named src/main.rs, rustc - // requires there to be a function named `main` somewhere in it. This - // requirement can be disabled using `#![no_main]`, however we can't - // use that automatically because macros can't contain inner - // attributes, and we don't want to require users to add `#![no_main]` - // in their own code. - // - // So, we include a definition of a function named `main` here, which - // isn't intended to ever be called, and exists just to satify the - // requirement for a `main` function. - // - // Users could use `#![no_main]` if they want to. Or, they could name - // their top-level file src/lib.rs and add - // ```toml - // [lib] - // crate-type = ["cdylib"] - // ``` - // to their Cargo.toml. With either of these, this "main" function will - // be ignored as dead code. fn main() { unreachable!("HTTP server components should be run with `handle` rather than `run`") } diff --git a/src/http/body.rs b/src/http/body.rs index c23a3c0..20e3c66 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,10 +1,5 @@ -use crate::http::{ - Error, HeaderMap, - error::Context as _, - fields::{header_map_from_wasi, header_map_to_wasi}, -}; -use crate::io::{AsyncInputStream, AsyncOutputStream}; -use crate::runtime::{AsyncPollable, Reactor, WaitFor}; +use crate::http::{Error, HeaderMap, error::Context as _}; +use crate::io::AsyncInputStream; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; pub use bytes::Bytes; @@ -12,12 +7,26 @@ pub use bytes::Bytes; use http::header::CONTENT_LENGTH; use http_body_util::{BodyExt, combinators::UnsyncBoxBody}; use std::fmt; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use crate::http::fields::{header_map_from_wasi, header_map_to_wasi}; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use crate::io::AsyncOutputStream; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use std::future::{Future, poll_fn}; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use std::pin::{Pin, pin}; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use std::task::{Context, Poll}; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use crate::runtime::{AsyncPollable, Reactor, WaitFor}; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::http::types::{ FutureTrailers, IncomingBody as WasiIncomingBody, OutgoingBody as WasiOutgoingBody, }; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::io::streams::{InputStream as WasiInputStream, StreamError}; pub mod util { @@ -60,8 +69,12 @@ pub struct Body(BodyInner); enum BodyInner { // a boxed http_body::Body impl Boxed(UnsyncBoxBody), - // a body created from a wasi-http incoming-body (WasiIncomingBody) + // a body created from a wasi-http incoming-body (p2) + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] Incoming(Incoming), + // a body created from a p3 StreamReader + #[cfg(feature = "wasip3")] + P3Stream(P3StreamBody), // a body in memory Complete { data: Bytes, @@ -69,6 +82,7 @@ enum BodyInner { }, } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl Body { pub(crate) async fn send(self, outgoing_body: WasiOutgoingBody) -> Result<(), Error> { match self.0 { @@ -122,16 +136,44 @@ impl Body { } } - /// Convert this `Body` into an `UnsyncBoxBody`, which - /// exists to implement the `http_body::Body` trait. Consume the contents - /// using `http_body_utils::BodyExt`, or anywhere else an impl of - /// `http_body::Body` is accepted. + pub(crate) fn from_incoming(body: WasiIncomingBody, size_hint: BodyHint) -> Self { + Body(BodyInner::Incoming(Incoming { body, size_hint })) + } +} + +#[cfg(feature = "wasip3")] +impl Body { + pub(crate) fn from_p3_stream( + reader: wit_bindgen::rt::async_support::StreamReader, + size_hint: BodyHint, + ) -> Self { + Body(BodyInner::P3Stream(P3StreamBody { + reader: Some(reader), + size_hint, + })) + } +} + +impl Body { + /// Convert this `Body` into an `UnsyncBoxBody`. pub fn into_boxed_body(self) -> UnsyncBoxBody { fn map_e(_: std::convert::Infallible) -> Error { unreachable!() } match self.0 { + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(), + #[cfg(feature = "wasip3")] + BodyInner::P3Stream(p3) => { + // Convert p3 stream body to a boxed body + let stream = AsyncInputStream::new(p3.reader.unwrap()); + use futures_lite::stream::StreamExt; + http_body_util::StreamBody::new(stream.into_stream().map(|res| { + res.map(|bytevec| Frame::data(Bytes::from_owner(bytevec))) + .map_err(Into::into) + })) + .boxed_unsync() + } BodyInner::Complete { data, trailers } => http_body_util::Full::new(data) .map_err(map_e) .with_trailers(async move { Ok(trailers).transpose() }) @@ -152,10 +194,39 @@ impl Body { trailers: None, }; std::mem::swap(inner, &mut prev); + + // For p3 streams, read directly using the async read method + // instead of going through poll_next (which doesn't properly + // persist the read future across polls, causing hangs). + #[cfg(feature = "wasip3")] + if let BodyInner::P3Stream(p3) = prev { + let mut stream = AsyncInputStream::new(p3.reader.unwrap()); + let mut all_data = Vec::new(); + let mut buf = vec![0u8; 64 * 1024]; + loop { + match stream.read(&mut buf).await { + Ok(0) => break, + Ok(n) => all_data.extend_from_slice(&buf[..n]), + Err(e) => return Err(Error::from(e).context("reading p3 body stream")), + } + } + *inner = BodyInner::Complete { + data: Bytes::from(all_data), + trailers: None, + }; + return Ok(match inner { + BodyInner::Complete { data, .. } => &*data, + _ => unreachable!(), + }); + } + let boxed_body = match prev { + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(), BodyInner::Boxed(b) => b, BodyInner::Complete { .. } => unreachable!(), + #[cfg(feature = "wasip3")] + BodyInner::P3Stream(_) => unreachable!(), }; let collected = boxed_body.collect().await?; let trailers = collected.trailers().cloned(); @@ -180,7 +251,10 @@ impl Body { match &self.0 { BodyInner::Boxed(b) => b.size_hint().exact(), BodyInner::Complete { data, .. } => Some(data.len() as u64), + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] BodyInner::Incoming(i) => i.size_hint.content_length(), + #[cfg(feature = "wasip3")] + BodyInner::P3Stream(p3) => p3.size_hint.content_length(), } } @@ -217,12 +291,7 @@ impl Body { serde_json::from_str(str).context("decoding body contents as json") } - pub(crate) fn from_incoming(body: WasiIncomingBody, size_hint: BodyHint) -> Self { - Body(BodyInner::Incoming(Incoming { body, size_hint })) - } - - /// Construct a `Body` backed by a `futures_lite::Stream` impl. The stream - /// will be polled as the body is sent. + /// Construct a `Body` backed by a `futures_lite::Stream` impl. pub fn from_stream(stream: S) -> Self where S: futures_lite::Stream + Send + 'static, @@ -323,12 +392,62 @@ impl From for Body { } } +#[derive(Clone, Copy, Debug)] +pub enum BodyHint { + ContentLength(u64), + Unknown, +} + +impl BodyHint { + pub fn from_headers(headers: &HeaderMap) -> Result { + if let Some(val) = headers.get(CONTENT_LENGTH) { + let len = std::str::from_utf8(val.as_ref()) + .map_err(|_| InvalidContentLength)? + .parse::() + .map_err(|_| InvalidContentLength)?; + Ok(BodyHint::ContentLength(len)) + } else { + Ok(BodyHint::Unknown) + } + } + fn content_length(&self) -> Option { + match self { + BodyHint::ContentLength(l) => Some(*l), + _ => None, + } + } +} +#[derive(Debug)] +pub struct InvalidContentLength; +impl fmt::Display for InvalidContentLength { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid Content-Length header") + } +} +impl std::error::Error for InvalidContentLength {} + +#[cfg(feature = "wasip3")] +struct P3StreamBody { + reader: Option>, + size_hint: BodyHint, +} + +#[cfg(feature = "wasip3")] +impl fmt::Debug for P3StreamBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("P3StreamBody").finish() + } +} + + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug)] struct Incoming { body: WasiIncomingBody, size_hint: BodyHint, } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl Incoming { fn into_http_body(self) -> IncomingBody { IncomingBody::new(self.body, self.size_hint) @@ -363,46 +482,14 @@ impl Incoming { } } -#[derive(Clone, Copy, Debug)] -pub enum BodyHint { - ContentLength(u64), - Unknown, -} - -impl BodyHint { - pub fn from_headers(headers: &HeaderMap) -> Result { - if let Some(val) = headers.get(CONTENT_LENGTH) { - let len = std::str::from_utf8(val.as_ref()) - .map_err(|_| InvalidContentLength)? - .parse::() - .map_err(|_| InvalidContentLength)?; - Ok(BodyHint::ContentLength(len)) - } else { - Ok(BodyHint::Unknown) - } - } - fn content_length(&self) -> Option { - match self { - BodyHint::ContentLength(l) => Some(*l), - _ => None, - } - } -} -#[derive(Debug)] -pub struct InvalidContentLength; -impl fmt::Display for InvalidContentLength { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Invalid Content-Length header") - } -} -impl std::error::Error for InvalidContentLength {} - +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug)] pub struct IncomingBody { state: Option>>, size_hint: BodyHint, } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl IncomingBody { fn new(body: WasiIncomingBody, size_hint: BodyHint) -> Self { Self { @@ -421,6 +508,7 @@ impl IncomingBody { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl HttpBody for IncomingBody { type Data = Bytes; type Error = Error; @@ -476,6 +564,7 @@ impl HttpBody for IncomingBody { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pin_project_lite::pin_project! { #[project = IBSProj] #[derive(Debug)] @@ -494,6 +583,7 @@ pin_project_lite::pin_project! { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug)] struct BodyState { wait: Option>>, @@ -501,8 +591,10 @@ struct BodyState { stream: WasiInputStream, } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] const MAX_FRAME_SIZE: u64 = 64 * 1024; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl BodyState { fn poll_frame( mut self: Pin<&mut Self>, @@ -547,6 +639,7 @@ impl BodyState { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug)] struct TrailersState { wait: Option>>, @@ -554,6 +647,7 @@ struct TrailersState { future_trailers: FutureTrailers, } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl TrailersState { fn new(future_trailers: FutureTrailers) -> Self { Self { diff --git a/src/http/client.rs b/src/http/client.rs index 3676fa8..4d724a7 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,9 +1,5 @@ use super::{Body, Error, Request, Response}; -use crate::http::request::try_into_outgoing; -use crate::http::response::try_from_incoming; -use crate::io::AsyncPollable; use crate::time::Duration; -use wasip2::http::types::RequestOptions as WasiRequestOptions; /// An HTTP client. #[derive(Debug, Clone)] @@ -24,34 +20,58 @@ impl Client { } /// Send an HTTP request. + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub async fn send>(&self, req: Request) -> Result, Error> { + use crate::http::request::try_into_outgoing; + use crate::http::response::try_from_incoming; + use crate::io::AsyncPollable; let (wasi_req, body) = try_into_outgoing(req)?; let body = body.into(); let wasi_body = wasi_req.body().unwrap(); - // 1. Start sending the request head - let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options()?)?; + let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options_p2()?)?; - let ((), body) = futures_lite::future::try_zip( - async move { - // 3. send the body: - body.send(wasi_body).await - }, - async move { - // 4. Receive the response + let ((), body) = + futures_lite::future::try_zip(async move { body.send(wasi_body).await }, async move { AsyncPollable::new(res.subscribe()).wait_for().await; - - // NOTE: the first `unwrap` is to ensure readiness, the second `unwrap` - // is to trap if we try and get the response more than once. The final - // `?` is to raise the actual error if there is one. let res = res.get().unwrap().unwrap()?; try_from_incoming(res) - }, - ) - .await?; + }) + .await?; Ok(body) } + /// Send an HTTP request. + #[cfg(feature = "wasip3")] + pub async fn send>(&self, req: Request) -> Result, Error> { + use crate::http::request::try_into_wasi_request; + use crate::http::response::try_from_wasi_response; + + let parts = try_into_wasi_request(req, self.options.as_ref())?; + + // Send body data through the stream writer + if let Some(mut body_writer) = parts.body_writer { + let mut body = parts.body; + let body_bytes = body.contents().await?; + if !body_bytes.is_empty() { + let remaining = body_writer.write_all(body_bytes.to_vec()).await; + if !remaining.is_empty() { + return Err(anyhow::anyhow!("failed to write full request body")); + } + } + drop(body_writer); + } + + let wasi_resp = wasip3::http::client::send(parts.request).await?; + + // Create a completion future for consuming the response body + let (_completion_writer, completion_reader) = + wasip3::wit_future::new::>(|| Ok(())); + drop(_completion_writer); + + try_from_wasi_response(wasi_resp, completion_reader) + } + /// Set timeout on connecting to HTTP server pub fn set_connect_timeout(&mut self, d: impl Into) { self.options_mut().connect_timeout = Some(d.into()); @@ -77,24 +97,31 @@ impl Client { } } - fn wasi_options(&self) -> Result, crate::http::Error> { + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + fn wasi_options_p2( + &self, + ) -> Result, crate::http::Error> { self.options .as_ref() - .map(RequestOptions::to_wasi) + .map(RequestOptions::to_wasi_p2) .transpose() } } +#[cfg(feature = "wasip3")] +pub(crate) type P3RequestOptions = RequestOptions; + #[derive(Default, Debug, Clone)] -struct RequestOptions { - connect_timeout: Option, - first_byte_timeout: Option, - between_bytes_timeout: Option, +pub(crate) struct RequestOptions { + pub(crate) connect_timeout: Option, + pub(crate) first_byte_timeout: Option, + pub(crate) between_bytes_timeout: Option, } impl RequestOptions { - fn to_wasi(&self) -> Result { - let wasi = WasiRequestOptions::new(); + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + fn to_wasi_p2(&self) -> Result { + let wasi = wasip2::http::types::RequestOptions::new(); if let Some(timeout) = self.connect_timeout { wasi.set_connect_timeout(Some(timeout.0)).map_err(|()| { anyhow::Error::msg( diff --git a/src/http/error.rs b/src/http/error.rs index a4f22b0..ab90e2a 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -6,8 +6,13 @@ pub use crate::http::body::InvalidContentLength; pub use anyhow::Context; pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; pub use http::method::InvalidMethod; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub use wasip2::http::types::{ErrorCode, HeaderError}; +#[cfg(feature = "wasip3")] +pub use wasip3::http::types::{ErrorCode, HeaderError}; + pub type Error = anyhow::Error; /// The `http` result type. pub type Result = std::result::Result; diff --git a/src/http/fields.rs b/src/http/fields.rs index de6df16..4ebda6b 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,8 +1,14 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; use super::{Error, error::Context}; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::http::types::Fields; +#[cfg(feature = "wasip3")] +use wasip3::http::types::Fields; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { let mut output = HeaderMap::new(); for (key, value) in wasi_fields.entries() { @@ -15,6 +21,19 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result Result { + let mut output = HeaderMap::new(); + for (key, value) in wasi_fields.copy_all() { + let key = + HeaderName::from_bytes(key.as_bytes()).with_context(|| format!("header name {key}"))?; + let value = + HeaderValue::from_bytes(&value).with_context(|| format!("header value for {key}"))?; + output.append(key, value); + } + Ok(output) +} + pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { let wasi_fields = Fields::new(); for (key, value) in header_map { diff --git a/src/http/method.rs b/src/http/method.rs index d1882a8..49ffb59 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,5 +1,9 @@ +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::http::types::Method as WasiMethod; +#[cfg(feature = "wasip3")] +use wasip3::http::types::Method as WasiMethod; + pub use http::Method; use http::method::InvalidMethod; diff --git a/src/http/mod.rs b/src/http/mod.rs index 39f0a40..ea30a74 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -23,3 +23,138 @@ pub mod request; pub mod response; mod scheme; pub mod server; + +// Conditionally-compiled declarative macro for HTTP server export +// +// The `#[wstd::http_server]` proc macro delegates to this declarative macro. +// Because `#[macro_export]` macros are compiled in wstd's context, the `cfg` +// checks here use wstd's own feature flags so consumers don't need to define +// `wasip2`/`wasip3` features themselves. + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[macro_export] +#[doc(hidden)] +macro_rules! __http_server_export { + (@async { $($run_fn:tt)* }) => { + const _: () = { + struct TheServer; + + impl $crate::__internal::wasip2::exports::http::incoming_handler::Guest for TheServer { + fn handle( + wasi_request: $crate::__internal::wasip2::http::types::IncomingRequest, + response_out: $crate::__internal::wasip2::http::types::ResponseOutparam + ) { + $($run_fn)* + + let responder = $crate::http::server::Responder::new(response_out); + $crate::runtime::block_on(async move { + match $crate::http::request::try_from_incoming(wasi_request) { + ::core::result::Result::Ok(request) => match __run(request).await { + ::core::result::Result::Ok(response) => { responder.respond(response).await.unwrap() }, + ::core::result::Result::Err(err) => responder.fail(err), + } + ::core::result::Result::Err(err) => responder.fail(err), + } + }) + } + } + + $crate::__internal::wasip2::http::proxy::export!(TheServer with_types_in $crate::__internal::wasip2); + }; + }; + (@sync { $($run_fn:tt)* }) => { + const _: () = { + struct TheServer; + + impl $crate::__internal::wasip2::exports::http::incoming_handler::Guest for TheServer { + fn handle( + wasi_request: $crate::__internal::wasip2::http::types::IncomingRequest, + response_out: $crate::__internal::wasip2::http::types::ResponseOutparam + ) { + $($run_fn)* + + let responder = $crate::http::server::Responder::new(response_out); + $crate::runtime::block_on(async move { + match $crate::http::request::try_from_incoming(wasi_request) { + ::core::result::Result::Ok(request) => match __run(request) { + ::core::result::Result::Ok(response) => { responder.respond(response).await.unwrap() }, + ::core::result::Result::Err(err) => responder.fail(err), + } + ::core::result::Result::Err(err) => responder.fail(err), + } + }) + } + } + + $crate::__internal::wasip2::http::proxy::export!(TheServer with_types_in $crate::__internal::wasip2); + }; + }; +} + +#[cfg(feature = "wasip3")] +#[macro_export] +#[doc(hidden)] +macro_rules! __http_server_export { + (@async { $($run_fn:tt)* }) => { + const _: () = { + struct TheServer; + + impl $crate::__internal::wasip3::exports::http::handler::Guest for TheServer { + async fn handle( + wasi_request: $crate::__internal::wasip3::http::types::Request, + ) -> ::core::result::Result< + $crate::__internal::wasip3::http::types::Response, + $crate::__internal::wasip3::http::types::ErrorCode, + > { + $($run_fn)* + + let (_writer, completion_reader) = $crate::__internal::wasip3::wit_future::new::< + ::core::result::Result<(), $crate::__internal::wasip3::http::types::ErrorCode>, + >(|| ::core::result::Result::Ok(())); + ::core::mem::drop(_writer); + + let request = $crate::http::request::try_from_wasi_request(wasi_request, completion_reader) + .map_err($crate::http::server::error_to_wasi)?; + + let response = __run(request).await + .map_err($crate::http::server::error_to_wasi)?; + + $crate::http::server::response_to_wasi(response).await + } + } + + $crate::__internal::wasip3::http::service::export!(TheServer with_types_in $crate::__internal::wasip3); + }; + }; + (@sync { $($run_fn:tt)* }) => { + const _: () = { + struct TheServer; + + impl $crate::__internal::wasip3::exports::http::handler::Guest for TheServer { + async fn handle( + wasi_request: $crate::__internal::wasip3::http::types::Request, + ) -> ::core::result::Result< + $crate::__internal::wasip3::http::types::Response, + $crate::__internal::wasip3::http::types::ErrorCode, + > { + $($run_fn)* + + let (_writer, completion_reader) = $crate::__internal::wasip3::wit_future::new::< + ::core::result::Result<(), $crate::__internal::wasip3::http::types::ErrorCode>, + >(|| ::core::result::Result::Ok(())); + ::core::mem::drop(_writer); + + let request = $crate::http::request::try_from_wasi_request(wasi_request, completion_reader) + .map_err($crate::http::server::error_to_wasi)?; + + let response = __run(request) + .map_err($crate::http::server::error_to_wasi)?; + + $crate::http::server::response_to_wasi(response).await + } + } + + $crate::__internal::wasip3::http::service::export!(TheServer with_types_in $crate::__internal::wasip3); + }; + }; +} diff --git a/src/http/request.rs b/src/http/request.rs index 6694d03..12ffd87 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -6,13 +6,16 @@ use super::{ method::{from_wasi_method, to_wasi_method}, scheme::{from_wasi_scheme, to_wasi_scheme}, }; -use wasip2::http::outgoing_handler::OutgoingRequest; -use wasip2::http::types::IncomingRequest; pub use http::request::{Builder, Request}; -// TODO: go back and add json stuff??? +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use wasip2::http::outgoing_handler::OutgoingRequest; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use wasip2::http::types::IncomingRequest; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); @@ -53,6 +56,7 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque /// This is used by the `http_server` macro. #[doc(hidden)] +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers()) .context("headers provided by wasi rejected by http::HeaderMap")?; @@ -82,8 +86,6 @@ pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Err let hint = BodyHint::from_headers(&headers)?; - // `body_stream` is a child of `incoming_body` which means we cannot - // drop the parent before we drop the child let incoming_body = incoming .consume() .expect("`consume` should not have been called previously on this incoming-request"); @@ -107,3 +109,153 @@ pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Err } request.body(body).context("building request from wasi") } + + +#[cfg(feature = "wasip3")] +use wasip3::http::types::{ + Request as WasiRequest, RequestOptions as WasiRequestOptions, Scheme as WasiScheme, +}; + +/// Result of converting an http::Request into a p3 WASI Request. +#[cfg(feature = "wasip3")] +pub(crate) struct WasiRequestParts { + pub request: WasiRequest, + pub body: Body, + pub body_writer: Option>, + pub _completion: wit_bindgen::rt::async_support::FutureReader>, +} + +/// Convert an http::Request into a p3 WASI Request for sending. +#[cfg(feature = "wasip3")] +pub(crate) fn try_into_wasi_request>( + request: Request, + request_options: Option<&super::client::P3RequestOptions>, +) -> Result { + let headers = header_map_to_wasi(request.headers())?; + let (parts, body) = request.into_parts(); + let body: Body = body.into(); + + // Create trailers future (no trailers for now) + let (trailers_writer, trailers_reader) = wasip3::wit_future::new::< + Result, ErrorCode>, + >(|| Ok(None)); + drop(trailers_writer); + + // Create body stream — keep the writer for the caller to send body data + let (body_writer, body_reader) = if body.content_length() == Some(0) { + (None, None) + } else { + let (writer, reader) = wasip3::wit_stream::new::(); + (Some(writer), Some(reader)) + }; + + let options = WasiRequestOptions::new(); + if let Some(opts) = request_options { + if let Some(timeout) = opts.connect_timeout { + let _ = options.set_connect_timeout(Some(timeout.0)); + } + if let Some(timeout) = opts.first_byte_timeout { + let _ = options.set_first_byte_timeout(Some(timeout.0)); + } + if let Some(timeout) = opts.between_bytes_timeout { + let _ = options.set_between_bytes_timeout(Some(timeout.0)); + } + } + + let (wasi_req, completion) = + WasiRequest::new(headers, body_reader, trailers_reader, Some(options)); + + // Set the HTTP method + let method = to_wasi_method(parts.method); + wasi_req + .set_method(&method) + .map_err(|()| anyhow::anyhow!("method rejected by wasi-http: {method:?}"))?; + + // Set the url scheme + let scheme = parts + .uri + .scheme() + .map(to_wasi_scheme) + .unwrap_or(WasiScheme::Https); + wasi_req + .set_scheme(Some(&scheme)) + .map_err(|()| anyhow::anyhow!("scheme rejected by wasi-http: {scheme:?}"))?; + + // Set authority + let authority = parts.uri.authority().map(Authority::as_str); + wasi_req + .set_authority(authority) + .map_err(|()| anyhow::anyhow!("authority rejected by wasi-http {authority:?}"))?; + + // Set the url path + query string + if let Some(p_and_q) = parts.uri.path_and_query() { + wasi_req + .set_path_with_query(Some(p_and_q.as_str())) + .map_err(|()| anyhow::anyhow!("path and query rejected by wasi-http {p_and_q:?}"))?; + } + + Ok(WasiRequestParts { + request: wasi_req, + body, + body_writer, + _completion: completion, + }) +} + +/// Convert a p3 WASI Request into an http::Request (for the server handler). +#[doc(hidden)] +#[cfg(feature = "wasip3")] +pub fn try_from_wasi_request( + incoming: WasiRequest, + completion: wit_bindgen::rt::async_support::FutureReader>, +) -> Result, Error> { + let headers: HeaderMap = header_map_from_wasi(incoming.get_headers()) + .context("headers provided by wasi rejected by http::HeaderMap")?; + + let method = + from_wasi_method(incoming.get_method()).map_err(|_| ErrorCode::HttpRequestMethodInvalid)?; + let scheme = incoming + .get_scheme() + .map(|scheme| { + from_wasi_scheme(scheme).context("scheme provided by wasi rejected by http::Scheme") + }) + .transpose()?; + let authority = incoming + .get_authority() + .map(|authority| { + Authority::from_maybe_shared(authority) + .context("authority provided by wasi rejected by http::Authority") + }) + .transpose()?; + let path_and_query = incoming + .get_path_with_query() + .map(|path_and_query| { + PathAndQuery::from_maybe_shared(path_and_query) + .context("path and query provided by wasi rejected by http::PathAndQuery") + }) + .transpose()?; + + let hint = BodyHint::from_headers(&headers)?; + + // Consume the request body + let (body_stream, _trailers_future) = WasiRequest::consume_body(incoming, completion); + let body = Body::from_p3_stream(body_stream, hint); + + let mut uri = Uri::builder(); + if let Some(scheme) = scheme { + uri = uri.scheme(scheme); + } + if let Some(authority) = authority { + uri = uri.authority(authority); + } + if let Some(path_and_query) = path_and_query { + uri = uri.path_and_query(path_and_query); + } + let uri = uri.build().context("building uri from wasi")?; + + let mut request = Request::builder().method(method).uri(uri); + if let Some(headers_mut) = request.headers_mut() { + *headers_mut = headers; + } + request.body(body).context("building request from wasi") +} diff --git a/src/http/response.rs b/src/http/response.rs index 2ab8d87..11d2f9f 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,5 +1,4 @@ use http::StatusCode; -use wasip2::http::types::IncomingResponse; use crate::http::body::{Body, BodyHint}; use crate::http::error::Error; @@ -7,30 +6,51 @@ use crate::http::fields::{HeaderMap, header_map_from_wasi}; pub use http::response::{Builder, Response}; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use wasip2::http::types::IncomingResponse; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; - // TODO: Does WASI guarantee that the incoming status is valid? let status = StatusCode::from_u16(incoming.status()) .map_err(|err| anyhow::anyhow!("wasi provided invalid status code ({err})"))?; let hint = BodyHint::from_headers(&headers)?; - // `body_stream` is a child of `incoming_body` which means we cannot - // drop the parent before we drop the child let incoming_body = incoming .consume() .expect("cannot call `consume` twice on incoming response"); let body = Body::from_incoming(incoming_body, hint); let mut builder = Response::builder().status(status); - // The [`http::response::Builder`] keeps internal state of whether the - // builder has errored, which is only reachable by passing - // [`Builder::header`] an erroring `TryInto` or - // `TryInto`. Since the `Builder::header` method is never - // used, we know `Builder::headers_mut` will never give the None case, nor - // will `Builder::body` give the error case. So, rather than treat those - // as control flow, we unwrap if this invariant is ever broken because - // that would only be possible due to some unrecoverable bug in wstd, - // rather than incorrect use or invalid input. + *builder.headers_mut().expect("builder has not errored") = headers; + Ok(builder + .body(body) + .expect("response builder should not error")) +} + + +#[cfg(feature = "wasip3")] +use crate::http::error::ErrorCode; +#[cfg(feature = "wasip3")] +use wasip3::http::types::Response as WasiResponse; + +#[cfg(feature = "wasip3")] +pub(crate) fn try_from_wasi_response( + incoming: WasiResponse, + completion: wit_bindgen::rt::async_support::FutureReader>, +) -> Result, Error> { + let headers: HeaderMap = header_map_from_wasi(incoming.get_headers())?; + let status = StatusCode::from_u16(incoming.get_status_code()) + .map_err(|err| anyhow::anyhow!("wasi provided invalid status code ({err})"))?; + + let hint = BodyHint::from_headers(&headers)?; + + // Consume the response body + let (body_stream, _trailers_future) = WasiResponse::consume_body(incoming, completion); + let body = Body::from_p3_stream(body_stream, hint); + + let mut builder = Response::builder().status(status); *builder.headers_mut().expect("builder has not errored") = headers; Ok(builder .body(body) diff --git a/src/http/scheme.rs b/src/http/scheme.rs index 8a3298e..fb27b60 100644 --- a/src/http/scheme.rs +++ b/src/http/scheme.rs @@ -1,5 +1,9 @@ +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::http::types::Scheme as WasiScheme; +#[cfg(feature = "wasip3")] +use wasip3::http::types::Scheme as WasiScheme; + pub use http::uri::{InvalidUri, Scheme}; use std::str::FromStr; diff --git a/src/http/server.rs b/src/http/server.rs index 9fb6ff4..3180bb0 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -18,70 +18,154 @@ //! [`Response`]: crate::http::Response //! [`http_server`]: crate::http_server -use super::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; -use http::header::CONTENT_LENGTH; -use wasip2::exports::http::incoming_handler::ResponseOutparam; -use wasip2::http::types::OutgoingResponse; - -/// For use by the [`http_server`] macro only. -/// -/// [`http_server`]: crate::http_server -#[doc(hidden)] -#[must_use] -pub struct Responder { - outparam: ResponseOutparam, + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +mod p2 { + use crate::http::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; + use http::header::CONTENT_LENGTH; + use wasip2::exports::http::incoming_handler::ResponseOutparam; + use wasip2::http::types::OutgoingResponse; + + /// For use by the [`http_server`] macro only. + /// + /// [`http_server`]: crate::http_server + #[doc(hidden)] + #[must_use] + pub struct Responder { + outparam: ResponseOutparam, + } + + impl Responder { + /// This is used by the `http_server` macro. + #[doc(hidden)] + pub async fn respond>(self, response: Response) -> Result<(), Error> { + let headers = response.headers(); + let status = response.status().as_u16(); + + let wasi_headers = header_map_to_wasi(headers).expect("header error"); + + let body = response.into_body().into(); + + if let Some(len) = body.content_length() { + let mut buffer = itoa::Buffer::new(); + wasi_headers + .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) + .unwrap(); + } + + let wasi_response = OutgoingResponse::new(wasi_headers); + wasi_response.set_status_code(status).unwrap(); + let wasi_body = wasi_response.body().unwrap(); + + ResponseOutparam::set(self.outparam, Ok(wasi_response)); + + body.send(wasi_body).await + } + + /// This is used by the `http_server` macro. + #[doc(hidden)] + pub fn new(outparam: ResponseOutparam) -> Self { + Self { outparam } + } + + /// This is used by the `http_server` macro. + #[doc(hidden)] + pub fn fail(self, err: Error) { + let e = match err.downcast_ref::() { + Some(e) => e.clone(), + None => ErrorCode::InternalError(Some(format!("{err:?}"))), + }; + ResponseOutparam::set(self.outparam, Err(e)); + } + } } -impl Responder { - /// This is used by the `http_server` macro. +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use p2::*; + +// In p3, the handler trait is `async fn handle(Request) -> Result`. +// The macro generates the appropriate code. No Responder/outparam pattern needed. + +// p3 server utilities for the macro +#[cfg(feature = "wasip3")] +pub use p3::*; + +#[cfg(feature = "wasip3")] +mod p3 { + use crate::http::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; + use http::header::CONTENT_LENGTH; + use wasip3::http::types::{Response as WasiResponse, Trailers}; + + /// Convert a wstd Response into a p3 WASI Response for the handler. #[doc(hidden)] - pub async fn respond>(self, response: Response) -> Result<(), Error> { + pub async fn response_to_wasi>( + response: Response, + ) -> Result { let headers = response.headers(); let status = response.status().as_u16(); - let wasi_headers = header_map_to_wasi(headers).expect("header error"); + let wasi_headers = header_map_to_wasi(headers) + .map_err(|_| ErrorCode::InternalError(Some("header error".to_string())))?; - // Consume the `response` and prepare to write the body. - let body = response.into_body().into(); + let mut body: Body = response.into_body().into(); - // Automatically add a Content-Length header. if let Some(len) = body.content_length() { let mut buffer = itoa::Buffer::new(); wasi_headers .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) - .unwrap(); + .map_err(|_| { + ErrorCode::InternalError(Some("content-length header error".to_string())) + })?; } - let wasi_response = OutgoingResponse::new(wasi_headers); + // Create body stream and write body data. + // The write must be spawned as a separate task because the stream reader + // can only make progress once the response is returned to the runtime. + // Writing inline would deadlock: write waits for reader, reader waits + // for response, response waits for write. + let body_bytes = body + .contents() + .await + .map_err(|e| ErrorCode::InternalError(Some(format!("collecting body: {e:?}"))))? + .to_vec(); - // Unwrap because `StatusCode` has already validated the status. - wasi_response.set_status_code(status).unwrap(); - - // Unwrap because we can be sure we only call these once. - let wasi_body = wasi_response.body().unwrap(); + let body_reader = if body_bytes.is_empty() { + None + } else { + let (writer, reader) = wasip3::wit_stream::new::(); + wit_bindgen::spawn(async move { + let mut writer = writer; + let remaining = writer.write_all(body_bytes).await; + if !remaining.is_empty() { + #[cfg(debug_assertions)] + panic!( + "response body write incomplete: {} bytes remaining", + remaining.len() + ); + } + }); + Some(reader) + }; - // Set the outparam to the response, which allows wasi-http to send - // the response status and headers. - ResponseOutparam::set(self.outparam, Ok(wasi_response)); + let (trailers_writer, trailers_reader) = + wasip3::wit_future::new::, ErrorCode>>(|| Ok(None)); + drop(trailers_writer); - // Then send the body. The response will be fully sent once this - // future is ready. - body.send(wasi_body).await - } + let (wasi_response, _completion) = + WasiResponse::new(wasi_headers, body_reader, trailers_reader); + wasi_response + .set_status_code(status) + .map_err(|()| ErrorCode::InternalError(Some("status code error".to_string())))?; - /// This is used by the `http_server` macro. - #[doc(hidden)] - pub fn new(outparam: ResponseOutparam) -> Self { - Self { outparam } + Ok(wasi_response) } - /// This is used by the `http_server` macro. + /// Convert an error to a p3 ErrorCode. #[doc(hidden)] - pub fn fail(self, err: Error) { - let e = match err.downcast_ref::() { + pub fn error_to_wasi(err: Error) -> ErrorCode { + match err.downcast_ref::() { Some(e) => e.clone(), None => ErrorCode::InternalError(Some(format!("{err:?}"))), - }; - ResponseOutparam::set(self.outparam, Err(e)); + } } } diff --git a/src/io/copy.rs b/src/io/copy.rs index 4fd178e..c65fc15 100644 --- a/src/io/copy.rs +++ b/src/io/copy.rs @@ -7,7 +7,8 @@ where W: AsyncWrite, { // Optimized path when we have an `AsyncInputStream` and an - // `AsyncOutputStream`. + // `AsyncOutputStream` (p2 only — p2 can use wasi splice). + #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] if let Some(reader) = reader.as_async_input_stream() && let Some(writer) = writer.as_async_output_stream() { diff --git a/src/io/mod.rs b/src/io/mod.rs index 0f34b1b..90fceee 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -9,6 +9,7 @@ mod stdio; mod streams; mod write; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub use crate::runtime::AsyncPollable; pub use copy::*; pub use cursor::*; diff --git a/src/io/stdio.rs b/src/io/stdio.rs index b2ac153..896bea7 100644 --- a/src/io/stdio.rs +++ b/src/io/stdio.rs @@ -1,8 +1,16 @@ use super::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Result}; use std::cell::LazyCell; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::cli::terminal_input::TerminalInput; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::cli::terminal_output::TerminalOutput; +#[cfg(feature = "wasip3")] +use wasip3::cli::terminal_input::TerminalInput; +#[cfg(feature = "wasip3")] +use wasip3::cli::terminal_output::TerminalOutput; + /// Use the program's stdin as an `AsyncInputStream`. #[derive(Debug)] pub struct Stdin { @@ -11,6 +19,7 @@ pub struct Stdin { } /// Get the program's stdin for use as an `AsyncInputStream`. +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub fn stdin() -> Stdin { let stream = AsyncInputStream::new(wasip2::cli::stdin::get_stdin()); Stdin { @@ -19,6 +28,17 @@ pub fn stdin() -> Stdin { } } +/// Get the program's stdin for use as an `AsyncInputStream`. +#[cfg(feature = "wasip3")] +pub fn stdin() -> Stdin { + let (reader, _completion) = wasip3::cli::stdin::read_via_stream(); + let stream = AsyncInputStream::new(reader); + Stdin { + stream, + terminput: LazyCell::new(wasip3::cli::terminal_stdin::get_terminal_stdin), + } +} + impl Stdin { /// Check if stdin is a terminal. pub fn is_terminal(&self) -> bool { @@ -56,6 +76,7 @@ pub struct Stdout { } /// Get the program's stdout for use as an `AsyncOutputStream`. +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub fn stdout() -> Stdout { let stream = AsyncOutputStream::new(wasip2::cli::stdout::get_stdout()); Stdout { @@ -64,6 +85,21 @@ pub fn stdout() -> Stdout { } } +/// Get the program's stdout for use as an `AsyncOutputStream`. +#[cfg(feature = "wasip3")] +pub fn stdout() -> Stdout { + let (writer, reader) = wasip3::wit_stream::new::(); + // Wire the reader end to the WASI stdout sink. The returned future resolves + // when the stream is fully consumed; we intentionally leak it so the pipe + // stays open for the lifetime of the program. + let _completion = wasip3::cli::stdout::write_via_stream(reader); + let stream = AsyncOutputStream::new(writer); + Stdout { + stream, + termoutput: LazyCell::new(wasip3::cli::terminal_stdout::get_terminal_stdout), + } +} + impl Stdout { /// Check if stdout is a terminal. pub fn is_terminal(&self) -> bool { @@ -98,14 +134,15 @@ impl AsyncWrite for Stdout { } } -/// Use the program's stdout as an `AsyncOutputStream`. +/// Use the program's stderr as an `AsyncOutputStream`. #[derive(Debug)] pub struct Stderr { stream: AsyncOutputStream, termoutput: LazyCell>, } -/// Get the program's stdout for use as an `AsyncOutputStream`. +/// Get the program's stderr for use as an `AsyncOutputStream`. +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub fn stderr() -> Stderr { let stream = AsyncOutputStream::new(wasip2::cli::stderr::get_stderr()); Stderr { @@ -114,6 +151,18 @@ pub fn stderr() -> Stderr { } } +/// Get the program's stderr for use as an `AsyncOutputStream`. +#[cfg(feature = "wasip3")] +pub fn stderr() -> Stderr { + let (writer, reader) = wasip3::wit_stream::new::(); + let _completion = wasip3::cli::stderr::write_via_stream(reader); + let stream = AsyncOutputStream::new(writer); + Stderr { + stream, + termoutput: LazyCell::new(wasip3::cli::terminal_stderr::get_terminal_stderr), + } +} + impl Stderr { /// Check if stderr is a terminal. pub fn is_terminal(&self) -> bool { diff --git a/src/io/streams.rs b/src/io/streams.rs index 3676d21..f253a5a 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -1,336 +1,558 @@ -use super::{AsyncPollable, AsyncRead, AsyncWrite}; -use crate::runtime::WaitFor; -use std::future::{Future, poll_fn}; -use std::pin::Pin; -use std::sync::{Mutex, OnceLock}; -use std::task::{Context, Poll}; -use wasip2::io::streams::{InputStream, OutputStream, StreamError}; - -/// A wrapper for WASI's `InputStream` resource that provides implementations of `AsyncRead` and -/// `AsyncPollable`. -#[derive(Debug)] -pub struct AsyncInputStream { - wait_for: Mutex>>>, - // Lazily initialized pollable, used for lifetime of stream to check readiness. - // Field ordering matters: this child must be dropped before stream - subscription: OnceLock, - stream: InputStream, -} +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +mod p2 { + use crate::io::{AsyncPollable, AsyncRead, AsyncWrite}; + use crate::runtime::WaitFor; + use std::future::{Future, poll_fn}; + use std::pin::Pin; + use std::sync::{Mutex, OnceLock}; + use std::task::{Context, Poll}; + use wasip2::io::streams::{InputStream, OutputStream, StreamError}; -impl AsyncInputStream { - /// Construct an `AsyncInputStream` from a WASI `InputStream` resource. - pub fn new(stream: InputStream) -> Self { - Self { - wait_for: Mutex::new(None), - subscription: OnceLock::new(), - stream, - } + /// A wrapper for WASI's `InputStream` resource that provides implementations of `AsyncRead` and + /// `AsyncPollable`. + #[derive(Debug)] + pub struct AsyncInputStream { + wait_for: Mutex>>>, + // Lazily initialized pollable, used for lifetime of stream to check readiness. + // Field ordering matters: this child must be dropped before stream + subscription: OnceLock, + stream: InputStream, } - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<()> { - // Lazily initialize the AsyncPollable - let subscription = self - .subscription - .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); - // Lazily initialize the WaitFor. Clear it after it becomes ready. - let mut wait_for_slot = self.wait_for.lock().unwrap(); - let wait_for = wait_for_slot.get_or_insert_with(|| Box::pin(subscription.wait_for())); - match wait_for.as_mut().poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(()) => { - let _ = wait_for_slot.take(); - Poll::Ready(()) + + impl AsyncInputStream { + /// Construct an `AsyncInputStream` from a WASI `InputStream` resource. + pub fn new(stream: InputStream) -> Self { + Self { + wait_for: Mutex::new(None), + subscription: OnceLock::new(), + stream, } } - } - /// Await for read readiness. - async fn ready(&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 { - 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())); + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<()> { + // Lazily initialize the AsyncPollable + let subscription = self + .subscription + .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); + // Lazily initialize the WaitFor. Clear it after it becomes ready. + let mut wait_for_slot = self.wait_for.lock().unwrap(); + let wait_for = wait_for_slot.get_or_insert_with(|| Box::pin(subscription.wait_for())); + match wait_for.as_mut().poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(()) => { + let _ = wait_for_slot.take(); + Poll::Ready(()) } } - }; - let len = read.len(); - buf[0..len].copy_from_slice(&read); - Ok(len) - } + } + /// Await for read readiness. + async fn ready(&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 { + let read = loop { + self.ready().await; + match self.stream.read(buf.len() as u64) { + Ok(r) if r.is_empty() => continue, + Ok(r) => break r, + 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. - pub async fn copy_to(&self, writer: &AsyncOutputStream) -> std::io::Result { - let mut written = 0; - loop { - self.ready().await; - writer.ready().await; - match writer.stream.splice(&self.stream, u64::MAX) { - Ok(n) => written += n, - Err(StreamError::Closed) => break Ok(written), - Err(StreamError::LastOperationFailed(err)) => { - break Err(std::io::Error::other(err.to_debug_string())); + /// Move the entire contents of an input stream directly into an output + /// stream, until the input stream has closed. + pub async fn copy_to(&self, writer: &AsyncOutputStream) -> std::io::Result { + let mut written = 0; + loop { + self.ready().await; + writer.ready().await; + match writer.stream.splice(&self.stream, u64::MAX) { + Ok(n) => written += n, + Err(StreamError::Closed) => break Ok(written), + Err(StreamError::LastOperationFailed(err)) => { + break Err(std::io::Error::other(err.to_debug_string())); + } } } } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result, std::io::Error>`. + pub fn into_stream(self) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size: 8 * 1024, + } + } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result, std::io::Error>`. The returned byte vectors + /// will be at most the `chunk_size` argument specified. + pub fn into_stream_of(self, chunk_size: usize) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size, + } + } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result`. + pub fn into_bytestream(self) -> AsyncInputByteStream { + AsyncInputByteStream { + stream: self.into_stream(), + buffer: std::io::Read::bytes(std::io::Cursor::new(Vec::new())), + } + } } - /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with - /// items of `Result, std::io::Error>`. The returned byte vectors - /// will be at most 8k. If you want to control chunk size, use - /// `Self::into_stream_of`. - pub fn into_stream(self) -> AsyncInputChunkStream { - AsyncInputChunkStream { - stream: self, - chunk_size: 8 * 1024, + impl AsyncRead for AsyncInputStream { + async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + Self::read(self, buf).await + } + + #[inline] + fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + Some(self) } } - /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with - /// items of `Result, std::io::Error>`. The returned byte vectors - /// will be at most the `chunk_size` argument specified. - pub fn into_stream_of(self, chunk_size: usize) -> AsyncInputChunkStream { - AsyncInputChunkStream { - stream: self, - chunk_size, + /// Wrapper of `AsyncInputStream` that impls `futures_lite::stream::Stream` + /// with an item of `Result, std::io::Error>` + pub struct AsyncInputChunkStream { + stream: AsyncInputStream, + chunk_size: usize, + } + + impl AsyncInputChunkStream { + /// Extract the `AsyncInputStream` which backs this stream. + pub fn into_inner(self) -> AsyncInputStream { + self.stream } } - /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with - /// items of `Result`. - pub fn into_bytestream(self) -> AsyncInputByteStream { - AsyncInputByteStream { - stream: self.into_stream(), - buffer: std::io::Read::bytes(std::io::Cursor::new(Vec::new())), + 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) { + Poll::Pending => Poll::Pending, + Poll::Ready(()) => match self.stream.stream.read(self.chunk_size as u64) { + Ok(r) if r.is_empty() => Poll::Pending, + Ok(r) => Poll::Ready(Some(Ok(r))), + Err(StreamError::LastOperationFailed(err)) => { + Poll::Ready(Some(Err(std::io::Error::other(err.to_debug_string())))) + } + Err(StreamError::Closed) => Poll::Ready(None), + }, + } } } -} -impl AsyncRead for AsyncInputStream { - async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - Self::read(self, buf).await + pin_project_lite::pin_project! { + /// Wrapper of `AsyncInputStream` that impls + /// `futures_lite::stream::Stream` with item `Result`. + pub struct AsyncInputByteStream { + #[pin] + stream: AsyncInputChunkStream, + buffer: std::io::Bytes>>, + } } - #[inline] - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - Some(self) + impl AsyncInputByteStream { + /// Extract the `AsyncInputStream` which backs this stream, and any bytes + /// read from the `AsyncInputStream` which have not yet been yielded by + /// the byte stream. + pub fn into_inner(self) -> (AsyncInputStream, Vec) { + ( + self.stream.into_inner(), + self.buffer + .collect::, std::io::Error>>() + .expect("read of Cursor> is infallible"), + ) + } } -} -/// Wrapper of `AsyncInputStream` that impls `futures_lite::stream::Stream` -/// with an item of `Result, std::io::Error>` -pub struct AsyncInputChunkStream { - stream: AsyncInputStream, - chunk_size: usize, -} + impl futures_lite::stream::Stream for AsyncInputByteStream { + type Item = Result; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + match this.buffer.next() { + Some(byte) => Poll::Ready(Some(Ok(byte.expect("cursor on Vec is infallible")))), + None => match futures_lite::stream::Stream::poll_next(this.stream, cx) { + Poll::Ready(Some(Ok(bytes))) => { + let mut bytes = std::io::Read::bytes(std::io::Cursor::new(bytes)); + match bytes.next() { + Some(Ok(byte)) => { + *this.buffer = bytes; + Poll::Ready(Some(Ok(byte))) + } + Some(Err(err)) => Poll::Ready(Some(Err(err))), + None => Poll::Ready(None), + } + } + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }, + } + } + } -impl AsyncInputChunkStream { - /// Extract the `AsyncInputStream` which backs this stream. - pub fn into_inner(self) -> AsyncInputStream { - self.stream + /// A wrapper for WASI's `output-stream` resource that provides implementations of `AsyncWrite` and + /// `AsyncPollable`. + #[derive(Debug)] + pub struct AsyncOutputStream { + // Lazily initialized pollable, used for lifetime of stream to check readiness. + // Field ordering matters: this child must be dropped before stream + subscription: OnceLock, + stream: OutputStream, } -} -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) { - Poll::Pending => Poll::Pending, - Poll::Ready(()) => match self.stream.stream.read(self.chunk_size as u64) { - Ok(r) if r.is_empty() => Poll::Pending, - Ok(r) => Poll::Ready(Some(Ok(r))), + impl AsyncOutputStream { + /// Construct an `AsyncOutputStream` from a WASI `OutputStream` resource. + pub fn new(stream: OutputStream) -> Self { + Self { + subscription: OnceLock::new(), + stream, + } + } + /// Await write readiness. + pub(crate) async fn ready(&self) { + let subscription = self + .subscription + .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); + 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`. + pub async fn write(&self, buf: &[u8]) -> std::io::Result { + loop { + match self.stream.check_write() { + Ok(0) => { + self.ready().await; + continue; + } + Ok(some) => { + let writable = some.try_into().unwrap_or(usize::MAX).min(buf.len()); + match self.stream.write(&buf[0..writable]) { + Ok(()) => return Ok(writable), + Err(StreamError::Closed) => { + return Err(std::io::Error::from( + std::io::ErrorKind::ConnectionReset, + )); + } + Err(StreamError::LastOperationFailed(err)) => { + return Err(std::io::Error::other(err.to_debug_string())); + } + } + } + Err(StreamError::Closed) => { + return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); + } + Err(StreamError::LastOperationFailed(err)) => { + return Err(std::io::Error::other(err.to_debug_string())); + } + } + } + } + + /// 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<()> { + 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. + pub async fn flush(&self) -> std::io::Result<()> { + match self.stream.flush() { + Ok(()) => { + self.ready().await; + Ok(()) + } + Err(StreamError::Closed) => { + Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)) + } Err(StreamError::LastOperationFailed(err)) => { - Poll::Ready(Some(Err(std::io::Error::other(err.to_debug_string())))) + Err(std::io::Error::other(err.to_debug_string())) } - Err(StreamError::Closed) => Poll::Ready(None), - }, + } } } -} -pin_project_lite::pin_project! { - /// Wrapper of `AsyncInputStream` that impls - /// `futures_lite::stream::Stream` with item `Result`. - pub struct AsyncInputByteStream { - #[pin] - stream: AsyncInputChunkStream, - buffer: std::io::Bytes>>, + impl AsyncWrite for AsyncOutputStream { + 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(&self) -> Option<&AsyncOutputStream> { + Some(self) + } } } -impl AsyncInputByteStream { - /// Extract the `AsyncInputStream` which backs this stream, and any bytes - /// read from the `AsyncInputStream` which have not yet been yielded by - /// the byte stream. - pub fn into_inner(self) -> (AsyncInputStream, Vec) { - ( - self.stream.into_inner(), - self.buffer - .collect::, std::io::Error>>() - .expect("read of Cursor> is infallible"), - ) +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use p2::*; + +#[cfg(feature = "wasip3")] +mod p3 { + use crate::io::{AsyncRead, AsyncWrite}; + use std::pin::Pin; + use std::task::{Context, Poll}; + use wit_bindgen::rt::async_support::{StreamReader, StreamResult, StreamWriter}; + + /// A wrapper for a p3 `StreamReader` that provides `AsyncRead`. + pub struct AsyncInputStream { + reader: StreamReader, } -} -impl futures_lite::stream::Stream for AsyncInputByteStream { - type Item = Result; - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.project(); - match this.buffer.next() { - Some(byte) => Poll::Ready(Some(Ok(byte.expect("cursor on Vec is infallible")))), - None => match futures_lite::stream::Stream::poll_next(this.stream, cx) { - Poll::Ready(Some(Ok(bytes))) => { - let mut bytes = std::io::Read::bytes(std::io::Cursor::new(bytes)); - match bytes.next() { - Some(Ok(byte)) => { - *this.buffer = bytes; - Poll::Ready(Some(Ok(byte))) - } - Some(Err(err)) => Poll::Ready(Some(Err(err))), - None => Poll::Ready(None), + impl std::fmt::Debug for AsyncInputStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AsyncInputStream").finish() + } + } + + impl AsyncInputStream { + /// Construct an `AsyncInputStream` from a p3 `StreamReader`. + pub fn new(reader: StreamReader) -> Self { + Self { reader } + } + + /// Asynchronously read from the input stream. + pub async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let read_buf = Vec::with_capacity(buf.len()); + let (result, data) = self.reader.read(read_buf).await; + match result { + StreamResult::Complete(_n) => { + if data.is_empty() { + // Stream ended + return Ok(0); } + let len = data.len(); + buf[0..len].copy_from_slice(&data); + Ok(len) } - Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - }, + StreamResult::Dropped => Ok(0), + StreamResult::Cancelled => Ok(0), + } + } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result, std::io::Error>`. + pub fn into_stream(self) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size: 8 * 1024, + } + } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result, std::io::Error>`. The returned byte vectors + /// will be at most the `chunk_size` argument specified. + pub fn into_stream_of(self, chunk_size: usize) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size, + } + } + + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with + /// items of `Result`. + pub fn into_bytestream(self) -> AsyncInputByteStream { + AsyncInputByteStream { + stream: self.into_stream(), + buffer: std::io::Read::bytes(std::io::Cursor::new(Vec::new())), + } + } + + /// Get a reference to the inner `StreamReader`. + pub fn inner(&self) -> &StreamReader { + &self.reader + } + + /// Consume this wrapper and return the inner `StreamReader`. + pub fn into_inner(self) -> StreamReader { + self.reader } } -} -/// A wrapper for WASI's `output-stream` resource that provides implementations of `AsyncWrite` and -/// `AsyncPollable`. -#[derive(Debug)] -pub struct AsyncOutputStream { - // Lazily initialized pollable, used for lifetime of stream to check readiness. - // Field ordering matters: this child must be dropped before stream - subscription: OnceLock, - stream: OutputStream, -} + impl AsyncRead for AsyncInputStream { + async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + AsyncInputStream::read(self, buf).await + } -impl AsyncOutputStream { - /// Construct an `AsyncOutputStream` from a WASI `OutputStream` resource. - pub fn new(stream: OutputStream) -> Self { - Self { - subscription: OnceLock::new(), - stream, + #[inline] + fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + Some(self) } } - /// Await write readiness. - async fn ready(&self) { - // Lazily initialize the AsyncPollable - let subscription = self - .subscription - .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); - // Wait on readiness - subscription.wait_for().await; + + /// Wrapper of `AsyncInputStream` that impls `futures_lite::stream::Stream` + pub struct AsyncInputChunkStream { + stream: AsyncInputStream, + chunk_size: usize, } - /// Asynchronously write to the output stream. This method is the same as - /// [`AsyncWrite::write`], but doesn't require a `&mut self`. - /// - /// 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 { - // Loops at most twice. - loop { - match self.stream.check_write() { - Ok(0) => { - self.ready().await; - // Next loop guaranteed to have nonzero check_write, or error. - continue; - } - Ok(some) => { - let writable = some.try_into().unwrap_or(usize::MAX).min(buf.len()); - match self.stream.write(&buf[0..writable]) { - Ok(()) => return Ok(writable), - Err(StreamError::Closed) => { - return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); - } - Err(StreamError::LastOperationFailed(err)) => { - return Err(std::io::Error::other(err.to_debug_string())); - } + + impl AsyncInputChunkStream { + pub fn into_inner(self) -> AsyncInputStream { + self.stream + } + } + + // Note: This is not a true poll-based stream for p3. We use a simple async approach. + // The `poll_next` implementation starts a read and awaits it inline. This works because + // in p3, the reads are natively async. + impl futures_lite::stream::Stream for AsyncInputChunkStream { + type Item = Result, std::io::Error>; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let read_buf = Vec::with_capacity(this.chunk_size); + let mut fut = std::pin::pin!(this.stream.reader.read(read_buf)); + match fut.as_mut().poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready((result, data)) => match result { + StreamResult::Complete(_) if data.is_empty() => { + // Try again, might have more data + Poll::Pending } - } - Err(StreamError::Closed) => { - return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); - } - Err(StreamError::LastOperationFailed(err)) => { - return Err(std::io::Error::other(err.to_debug_string())); - } + StreamResult::Complete(_) => Poll::Ready(Some(Ok(data))), + StreamResult::Dropped => Poll::Ready(None), + StreamResult::Cancelled => Poll::Ready(None), + }, } } } - /// 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<()> { - 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(()); + pin_project_lite::pin_project! { + /// Wrapper of `AsyncInputStream` that impls + /// `futures_lite::stream::Stream` with item `Result`. + pub struct AsyncInputByteStream { + #[pin] + stream: AsyncInputChunkStream, + buffer: std::io::Bytes>>, + } + } + + impl AsyncInputByteStream { + pub fn into_inner(self) -> (AsyncInputStream, Vec) { + ( + self.stream.into_inner(), + self.buffer + .collect::, std::io::Error>>() + .expect("read of Cursor> is infallible"), + ) + } + } + + impl futures_lite::stream::Stream for AsyncInputByteStream { + type Item = Result; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + match this.buffer.next() { + Some(byte) => Poll::Ready(Some(Ok(byte.expect("cursor on Vec is infallible")))), + None => match futures_lite::stream::Stream::poll_next(this.stream, cx) { + Poll::Ready(Some(Ok(bytes))) => { + let mut bytes = std::io::Read::bytes(std::io::Cursor::new(bytes)); + match bytes.next() { + Some(Ok(byte)) => { + *this.buffer = bytes; + Poll::Ready(Some(Ok(byte))) + } + Some(Err(err)) => Poll::Ready(Some(Err(err))), + None => Poll::Ready(None), + } + } + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }, } } } - /// 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. - /// - /// 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<()> { - match self.stream.flush() { - Ok(()) => { - self.ready().await; + /// A wrapper for a p3 `StreamWriter` that provides `AsyncWrite`. + pub struct AsyncOutputStream { + writer: StreamWriter, + } + + impl std::fmt::Debug for AsyncOutputStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AsyncOutputStream").finish() + } + } + + impl AsyncOutputStream { + /// Construct an `AsyncOutputStream` from a p3 `StreamWriter`. + pub fn new(writer: StreamWriter) -> Self { + Self { writer } + } + + /// Asynchronously write to the output stream. + pub async fn write(&mut self, buf: &[u8]) -> std::io::Result { + let data = buf.to_vec(); + let remaining = self.writer.write_all(data).await; + Ok(buf.len() - remaining.len()) + } + + /// Asynchronously write all bytes to the output stream. + pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + let data = buf.to_vec(); + let remaining = self.writer.write_all(data).await; + if remaining.is_empty() { Ok(()) - } - Err(StreamError::Closed) => { + } else { Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)) } - Err(StreamError::LastOperationFailed(err)) => { - Err(std::io::Error::other(err.to_debug_string())) - } } - } -} -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 + /// Flush the output stream (no-op for p3 streams). + pub async fn flush(&mut self) -> std::io::Result<()> { + // p3 streams don't have an explicit flush + Ok(()) + } + + /// Get a mutable reference to the inner `StreamWriter`. + pub fn inner_mut(&mut self) -> &mut StreamWriter { + &mut self.writer + } + + /// Consume this wrapper and return the inner `StreamWriter`. + pub fn into_inner(self) -> StreamWriter { + self.writer + } } - #[inline] - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - Some(self) + impl AsyncWrite for AsyncOutputStream { + async fn write(&mut self, buf: &[u8]) -> std::io::Result { + AsyncOutputStream::write(self, buf).await + } + async fn flush(&mut self) -> std::io::Result<()> { + AsyncOutputStream::flush(self).await + } + + #[inline] + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + Some(self) + } } } + +#[cfg(feature = "wasip3")] +pub use p3::*; diff --git a/src/lib.rs b/src/lib.rs index ebc673d..f3100d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,10 +70,17 @@ 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; -// Re-export the wasip2 crate for use only by `wstd-macro` macros. The proc +// Re-export the wasi bindings 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. +#[cfg(feature = "wasip3")] +#[doc(hidden)] +pub mod __internal { + pub use wasip3; +} + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[doc(hidden)] pub mod __internal { pub use wasip2; diff --git a/src/net/mod.rs b/src/net/mod.rs index 1600edc..e4b8b99 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1,7 +1,6 @@ //! Async network abstractions. use std::io::{self, ErrorKind}; -use wasip2::sockets::network::ErrorCode; mod tcp_listener; mod tcp_stream; @@ -9,7 +8,9 @@ mod tcp_stream; pub use tcp_listener::*; pub use tcp_stream::*; -fn to_io_err(err: ErrorCode) -> io::Error { +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +fn to_io_err(err: wasip2::sockets::network::ErrorCode) -> io::Error { + use wasip2::sockets::network::ErrorCode; match err { ErrorCode::Unknown => ErrorKind::Other.into(), ErrorCode::AccessDenied => ErrorKind::PermissionDenied.into(), @@ -27,3 +28,22 @@ fn to_io_err(err: ErrorCode) -> io::Error { _ => ErrorKind::Other.into(), } } + +#[cfg(feature = "wasip3")] +fn to_io_err(err: wasip3::sockets::types::ErrorCode) -> io::Error { + use wasip3::sockets::types::ErrorCode; + match err { + ErrorCode::AccessDenied => ErrorKind::PermissionDenied.into(), + ErrorCode::NotSupported => ErrorKind::Unsupported.into(), + ErrorCode::InvalidArgument => ErrorKind::InvalidInput.into(), + ErrorCode::OutOfMemory => ErrorKind::OutOfMemory.into(), + ErrorCode::Timeout => ErrorKind::TimedOut.into(), + ErrorCode::InvalidState => ErrorKind::InvalidData.into(), + ErrorCode::AddressInUse => ErrorKind::AddrInUse.into(), + ErrorCode::ConnectionRefused => ErrorKind::ConnectionRefused.into(), + ErrorCode::ConnectionReset => ErrorKind::ConnectionReset.into(), + ErrorCode::ConnectionAborted => ErrorKind::ConnectionAborted.into(), + ErrorCode::RemoteUnreachable => ErrorKind::HostUnreachable.into(), + _ => ErrorKind::Other.into(), + } +} diff --git a/src/net/tcp_listener.rs b/src/net/tcp_listener.rs index 9a1f57a..bedff09 100644 --- a/src/net/tcp_listener.rs +++ b/src/net/tcp_listener.rs @@ -1,129 +1,261 @@ -use wasip2::sockets::network::Ipv4SocketAddress; -use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress, TcpSocket}; - use crate::io; use crate::iter::AsyncIterator; use std::net::SocketAddr; use super::{TcpStream, to_io_err}; -use crate::runtime::AsyncPollable; - -/// A TCP socket server, listening for connections. -#[derive(Debug)] -pub struct TcpListener { - // Field order matters: must drop this child before parent below - pollable: AsyncPollable, - socket: TcpSocket, -} -impl TcpListener { - /// Creates a new TcpListener which will be bound to the specified address. - /// - /// The returned listener is ready for accepting connections. - pub async fn bind(addr: &str) -> io::Result { - let addr: SocketAddr = addr - .parse() - .map_err(|_| io::Error::other("failed to parse string to socket addr"))?; - let family = match addr { - SocketAddr::V4(_) => IpAddressFamily::Ipv4, - SocketAddr::V6(_) => IpAddressFamily::Ipv6, - }; - let socket = - wasip2::sockets::tcp_create_socket::create_tcp_socket(family).map_err(to_io_err)?; - let network = wasip2::sockets::instance_network::instance_network(); - - let local_address = sockaddr_to_wasi(addr); - - socket - .start_bind(&network, local_address) - .map_err(to_io_err)?; - let pollable = AsyncPollable::new(socket.subscribe()); - pollable.wait_for().await; - socket.finish_bind().map_err(to_io_err)?; - - socket.start_listen().map_err(to_io_err)?; - pollable.wait_for().await; - socket.finish_listen().map_err(to_io_err)?; - Ok(Self { pollable, socket }) - } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +mod p2 { + use super::*; + use crate::runtime::AsyncPollable; + use wasip2::sockets::network::Ipv4SocketAddress; + use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress, TcpSocket}; - /// Returns the local socket address of this listener. - pub fn local_addr(&self) -> io::Result { - self.socket - .local_address() - .map_err(to_io_err) - .map(sockaddr_from_wasi) + /// A TCP socket server, listening for connections. + #[derive(Debug)] + pub struct TcpListener { + // Field order matters: must drop this child before parent below + pollable: AsyncPollable, + socket: TcpSocket, } - /// Returns an iterator over the connections being received on this listener. - pub fn incoming(&self) -> Incoming<'_> { - Incoming { listener: self } + impl TcpListener { + /// Creates a new TcpListener which will be bound to the specified address. + pub async fn bind(addr: &str) -> io::Result { + let addr: SocketAddr = addr + .parse() + .map_err(|_| io::Error::other("failed to parse string to socket addr"))?; + let family = match addr { + SocketAddr::V4(_) => IpAddressFamily::Ipv4, + SocketAddr::V6(_) => IpAddressFamily::Ipv6, + }; + let socket = + wasip2::sockets::tcp_create_socket::create_tcp_socket(family).map_err(to_io_err)?; + let network = wasip2::sockets::instance_network::instance_network(); + + let local_address = sockaddr_to_wasi(addr); + + socket + .start_bind(&network, local_address) + .map_err(to_io_err)?; + let pollable = AsyncPollable::new(socket.subscribe()); + pollable.wait_for().await; + socket.finish_bind().map_err(to_io_err)?; + + socket.start_listen().map_err(to_io_err)?; + pollable.wait_for().await; + socket.finish_listen().map_err(to_io_err)?; + Ok(Self { pollable, socket }) + } + + /// Returns the local socket address of this listener. + pub fn local_addr(&self) -> io::Result { + self.socket + .local_address() + .map_err(to_io_err) + .map(sockaddr_from_wasi) + } + + /// Returns an iterator over the connections being received on this listener. + pub fn incoming(&self) -> Incoming<'_> { + Incoming { listener: self } + } } -} -/// An iterator that infinitely accepts connections on a TcpListener. -#[derive(Debug)] -pub struct Incoming<'a> { - listener: &'a TcpListener, -} + /// An iterator that infinitely accepts connections on a TcpListener. + #[derive(Debug)] + pub struct Incoming<'a> { + listener: &'a TcpListener, + } -impl<'a> AsyncIterator for Incoming<'a> { - type Item = io::Result; + impl<'a> AsyncIterator for Incoming<'a> { + type Item = io::Result; - async fn next(&mut self) -> Option { - self.listener.pollable.wait_for().await; - let (socket, input, output) = match self.listener.socket.accept().map_err(to_io_err) { - Ok(accepted) => accepted, - Err(err) => return Some(Err(err)), - }; - Some(Ok(TcpStream::new(input, output, socket))) + async fn next(&mut self) -> Option { + self.listener.pollable.wait_for().await; + let (socket, input, output) = match self.listener.socket.accept().map_err(to_io_err) { + Ok(accepted) => accepted, + Err(err) => return Some(Err(err)), + }; + Some(Ok(TcpStream::new(input, output, socket))) + } } -} -fn sockaddr_from_wasi(addr: IpSocketAddress) -> std::net::SocketAddr { - use wasip2::sockets::network::Ipv6SocketAddress; - match addr { - IpSocketAddress::Ipv4(Ipv4SocketAddress { address, port }) => { - std::net::SocketAddr::V4(std::net::SocketAddrV4::new( - std::net::Ipv4Addr::new(address.0, address.1, address.2, address.3), + fn sockaddr_from_wasi(addr: IpSocketAddress) -> std::net::SocketAddr { + use wasip2::sockets::network::Ipv6SocketAddress; + match addr { + IpSocketAddress::Ipv4(Ipv4SocketAddress { address, port }) => { + std::net::SocketAddr::V4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::new(address.0, address.1, address.2, address.3), + port, + )) + } + IpSocketAddress::Ipv6(Ipv6SocketAddress { + address, + port, + flow_info, + scope_id, + }) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new( + std::net::Ipv6Addr::new( + address.0, address.1, address.2, address.3, address.4, address.5, address.6, + address.7, + ), port, - )) + flow_info, + scope_id, + )), + } + } + + fn sockaddr_to_wasi(addr: std::net::SocketAddr) -> IpSocketAddress { + use wasip2::sockets::network::Ipv6SocketAddress; + match addr { + std::net::SocketAddr::V4(addr) => { + let ip = addr.ip().octets(); + IpSocketAddress::Ipv4(Ipv4SocketAddress { + address: (ip[0], ip[1], ip[2], ip[3]), + port: addr.port(), + }) + } + std::net::SocketAddr::V6(addr) => { + let ip = addr.ip().segments(); + IpSocketAddress::Ipv6(Ipv6SocketAddress { + address: (ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + }) + } } - IpSocketAddress::Ipv6(Ipv6SocketAddress { - address, - port, - flow_info, - scope_id, - }) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new( - std::net::Ipv6Addr::new( - address.0, address.1, address.2, address.3, address.4, address.5, address.6, - address.7, - ), - port, - flow_info, - scope_id, - )), } } -fn sockaddr_to_wasi(addr: std::net::SocketAddr) -> IpSocketAddress { - use wasip2::sockets::network::Ipv6SocketAddress; - match addr { - std::net::SocketAddr::V4(addr) => { - let ip = addr.ip().octets(); - IpSocketAddress::Ipv4(Ipv4SocketAddress { - address: (ip[0], ip[1], ip[2], ip[3]), - port: addr.port(), +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use p2::*; + +#[cfg(feature = "wasip3")] +mod p3 { + use super::*; + use wasip3::sockets::types::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, TcpSocket}; + use wit_bindgen::rt::async_support::StreamReader; + + /// A TCP socket server, listening for connections. + pub struct TcpListener { + accept_stream: StreamReader, + socket: TcpSocket, + } + + impl std::fmt::Debug for TcpListener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TcpListener").finish() + } + } + + impl TcpListener { + /// Creates a new TcpListener which will be bound to the specified address. + pub async fn bind(addr: &str) -> io::Result { + let addr: SocketAddr = addr + .parse() + .map_err(|_| io::Error::other("failed to parse string to socket addr"))?; + let family = match addr { + SocketAddr::V4(_) => IpAddressFamily::Ipv4, + SocketAddr::V6(_) => IpAddressFamily::Ipv6, + }; + let socket = TcpSocket::create(family).map_err(to_io_err)?; + let local_address = sockaddr_to_wasi(addr); + socket.bind(local_address).map_err(to_io_err)?; + let accept_stream = socket.listen().map_err(to_io_err)?; + Ok(Self { + accept_stream, + socket, }) } - std::net::SocketAddr::V6(addr) => { - let ip = addr.ip().segments(); + + /// Returns the local socket address of this listener. + pub fn local_addr(&self) -> io::Result { + self.socket + .get_local_address() + .map_err(to_io_err) + .map(sockaddr_from_wasi) + } + + /// Returns an iterator over the connections being received on this listener. + pub fn incoming(&mut self) -> Incoming<'_> { + Incoming { listener: self } + } + } + + /// An iterator that infinitely accepts connections on a TcpListener. + pub struct Incoming<'a> { + listener: &'a mut TcpListener, + } + + impl<'a> std::fmt::Debug for Incoming<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Incoming").finish() + } + } + + impl<'a> AsyncIterator for Incoming<'a> { + type Item = io::Result; + + async fn next(&mut self) -> Option { + self.listener + .accept_stream + .next() + .await + .map(TcpStream::from_connected_socket) + } + } + + fn sockaddr_from_wasi(addr: IpSocketAddress) -> std::net::SocketAddr { + use wasip3::sockets::types::Ipv6SocketAddress; + match addr { + IpSocketAddress::Ipv4(Ipv4SocketAddress { address, port }) => { + std::net::SocketAddr::V4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::new(address.0, address.1, address.2, address.3), + port, + )) + } IpSocketAddress::Ipv6(Ipv6SocketAddress { - address: (ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]), - port: addr.port(), - flow_info: addr.flowinfo(), - scope_id: addr.scope_id(), - }) + address, + port, + flow_info, + scope_id, + }) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new( + std::net::Ipv6Addr::new( + address.0, address.1, address.2, address.3, address.4, address.5, address.6, + address.7, + ), + port, + flow_info, + scope_id, + )), + } + } + + fn sockaddr_to_wasi(addr: std::net::SocketAddr) -> IpSocketAddress { + use wasip3::sockets::types::Ipv6SocketAddress; + match addr { + std::net::SocketAddr::V4(addr) => { + let ip = addr.ip().octets(); + IpSocketAddress::Ipv4(Ipv4SocketAddress { + address: (ip[0], ip[1], ip[2], ip[3]), + port: addr.port(), + }) + } + std::net::SocketAddr::V6(addr) => { + let ip = addr.ip().segments(); + IpSocketAddress::Ipv6(Ipv6SocketAddress { + address: (ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + }) + } } } } + +#[cfg(feature = "wasip3")] +pub use p3::*; diff --git a/src/net/tcp_stream.rs b/src/net/tcp_stream.rs index af3674a..3e5f63e 100644 --- a/src/net/tcp_stream.rs +++ b/src/net/tcp_stream.rs @@ -1,192 +1,329 @@ use std::io::ErrorKind; use std::net::{SocketAddr, ToSocketAddrs}; -use wasip2::sockets::instance_network::instance_network; -use wasip2::sockets::network::Ipv4SocketAddress; -use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress}; -use wasip2::sockets::tcp_create_socket::create_tcp_socket; -use wasip2::{ - io::streams::{InputStream, OutputStream}, - sockets::tcp::TcpSocket, -}; use super::to_io_err; use crate::io::{self, AsyncInputStream, AsyncOutputStream}; -use crate::runtime::AsyncPollable; -/// A TCP stream between a local and a remote socket. -pub struct TcpStream { - input: AsyncInputStream, - output: AsyncOutputStream, - socket: TcpSocket, -} +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +mod p2 { + use super::*; + use crate::runtime::AsyncPollable; + use wasip2::sockets::instance_network::instance_network; + use wasip2::sockets::network::Ipv4SocketAddress; + use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress}; + use wasip2::sockets::tcp_create_socket::create_tcp_socket; + use wasip2::{ + io::streams::{InputStream, OutputStream}, + sockets::tcp::TcpSocket, + }; + + /// A TCP stream between a local and a remote socket. + pub struct TcpStream { + input: AsyncInputStream, + output: AsyncOutputStream, + socket: TcpSocket, + } -impl TcpStream { - pub(crate) fn new(input: InputStream, output: OutputStream, socket: TcpSocket) -> Self { - TcpStream { - input: AsyncInputStream::new(input), - output: AsyncOutputStream::new(output), - socket, - } - } - - /// Opens a TCP connection to a remote host. - /// - /// `addr` is an address of the remote host. Anything which implements the - /// [`ToSocketAddrs`] trait can be supplied as the address. If `addr` - /// yields multiple addresses, connect will be attempted with each of the - /// addresses until a connection is successful. If none of the addresses - /// result in a successful connection, the error returned from the last - /// connection attempt (the last address) is returned. - pub async fn connect(addr: impl ToSocketAddrs) -> io::Result { - let addrs = addr.to_socket_addrs()?; - let mut last_err = None; - for addr in addrs { - match TcpStream::connect_addr(addr).await { - Ok(stream) => return Ok(stream), - Err(e) => last_err = Some(e), + impl TcpStream { + pub(crate) fn new(input: InputStream, output: OutputStream, socket: TcpSocket) -> Self { + TcpStream { + input: AsyncInputStream::new(input), + output: AsyncOutputStream::new(output), + socket, } } - Err(last_err.unwrap_or_else(|| { - io::Error::new(ErrorKind::InvalidInput, "could not resolve to any address") - })) - } + /// Opens a TCP connection to a remote host. + pub async fn connect(addr: impl ToSocketAddrs) -> io::Result { + let addrs = addr.to_socket_addrs()?; + let mut last_err = None; + for addr in addrs { + match TcpStream::connect_addr(addr).await { + Ok(stream) => return Ok(stream), + Err(e) => last_err = Some(e), + } + } - /// Establishes a connection to the specified `addr`. - pub async fn connect_addr(addr: SocketAddr) -> io::Result { - let family = match addr { - SocketAddr::V4(_) => IpAddressFamily::Ipv4, - SocketAddr::V6(_) => IpAddressFamily::Ipv6, - }; - let socket = create_tcp_socket(family).map_err(to_io_err)?; - let network = instance_network(); + Err(last_err.unwrap_or_else(|| { + io::Error::new(ErrorKind::InvalidInput, "could not resolve to any address") + })) + } - let remote_address = match addr { - SocketAddr::V4(addr) => { - let ip = addr.ip().octets(); - let address = (ip[0], ip[1], ip[2], ip[3]); - let port = addr.port(); - IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }) - } - SocketAddr::V6(_) => todo!("IPv6 not yet supported in `wstd::net::TcpStream`"), - }; - socket - .start_connect(&network, remote_address) - .map_err(to_io_err)?; - let pollable = AsyncPollable::new(socket.subscribe()); - pollable.wait_for().await; - let (input, output) = socket.finish_connect().map_err(to_io_err)?; + /// Establishes a connection to the specified `addr`. + pub async fn connect_addr(addr: SocketAddr) -> io::Result { + let family = match addr { + SocketAddr::V4(_) => IpAddressFamily::Ipv4, + SocketAddr::V6(_) => IpAddressFamily::Ipv6, + }; + let socket = create_tcp_socket(family).map_err(to_io_err)?; + let network = instance_network(); - Ok(TcpStream::new(input, output, socket)) - } + let remote_address = match addr { + SocketAddr::V4(addr) => { + let ip = addr.ip().octets(); + let address = (ip[0], ip[1], ip[2], ip[3]); + let port = addr.port(); + IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }) + } + SocketAddr::V6(_) => todo!("IPv6 not yet supported in `wstd::net::TcpStream`"), + }; + socket + .start_connect(&network, remote_address) + .map_err(to_io_err)?; + let pollable = AsyncPollable::new(socket.subscribe()); + pollable.wait_for().await; + let (input, output) = socket.finish_connect().map_err(to_io_err)?; - /// Returns the socket address of the remote peer of this TCP connection. - pub fn peer_addr(&self) -> io::Result { - let addr = self.socket.remote_address().map_err(to_io_err)?; - Ok(format!("{addr:?}")) - } + Ok(TcpStream::new(input, output, socket)) + } - pub fn split(&self) -> (ReadHalf<'_>, WriteHalf<'_>) { - (ReadHalf(self), WriteHalf(self)) - } -} + /// Returns the socket address of the remote peer of this TCP connection. + pub fn peer_addr(&self) -> io::Result { + let addr = self.socket.remote_address().map_err(to_io_err)?; + Ok(format!("{addr:?}")) + } -impl Drop for TcpStream { - fn drop(&mut self) { - let _ = self - .socket - .shutdown(wasip2::sockets::tcp::ShutdownType::Both); + pub fn split(&self) -> (ReadHalf<'_>, WriteHalf<'_>) { + (ReadHalf(self), WriteHalf(self)) + } } -} -impl io::AsyncRead for TcpStream { - async fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.input.read(buf).await + impl Drop for TcpStream { + fn drop(&mut self) { + let _ = self + .socket + .shutdown(wasip2::sockets::tcp::ShutdownType::Both); + } } - 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 + } -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> { + Some(&self.input) + } } - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - (**self).as_async_input_stream() + 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() + } } -} -impl io::AsyncWrite for TcpStream { - async fn write(&mut self, buf: &[u8]) -> io::Result { - self.output.write(buf).await + 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 + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + Some(&self.output) + } } - async fn flush(&mut self) -> io::Result<()> { - self.output.flush().await + 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 + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + (**self).as_async_output_stream() + } } - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - Some(&self.output) + 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 + } + + fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + self.0.as_async_input_stream() + } } -} -impl io::AsyncWrite for &TcpStream { - async fn write(&mut self, buf: &[u8]) -> io::Result { - self.output.write(buf).await + impl<'a> Drop for ReadHalf<'a> { + fn drop(&mut self) { + let _ = self + .0 + .socket + .shutdown(wasip2::sockets::tcp::ShutdownType::Receive); + } } - async fn flush(&mut self) -> io::Result<()> { - self.output.flush().await + 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 + } + + async fn flush(&mut self) -> io::Result<()> { + self.0.flush().await + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + self.0.as_async_output_stream() + } } - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - (**self).as_async_output_stream() + impl<'a> Drop for WriteHalf<'a> { + fn drop(&mut self) { + let _ = self + .0 + .socket + .shutdown(wasip2::sockets::tcp::ShutdownType::Send); + } } } -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 - } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use p2::*; + +#[cfg(feature = "wasip3")] +mod p3 { + use super::*; + use wasip3::sockets::types::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, TcpSocket}; - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - self.0.as_async_input_stream() + /// A TCP stream between a local and a remote socket. + pub struct TcpStream { + input: AsyncInputStream, + output: AsyncOutputStream, } -} -impl<'a> Drop for ReadHalf<'a> { - fn drop(&mut self) { - let _ = self - .0 - .socket - .shutdown(wasip2::sockets::tcp::ShutdownType::Receive); + impl TcpStream { + pub(crate) fn new(input: AsyncInputStream, output: AsyncOutputStream) -> Self { + TcpStream { input, output } + } + + /// Opens a TCP connection to a remote host. + pub async fn connect(addr: impl ToSocketAddrs) -> io::Result { + let addrs = addr.to_socket_addrs()?; + let mut last_err = None; + for addr in addrs { + match TcpStream::connect_addr(addr).await { + Ok(stream) => return Ok(stream), + Err(e) => last_err = Some(e), + } + } + + Err(last_err.unwrap_or_else(|| { + io::Error::new(ErrorKind::InvalidInput, "could not resolve to any address") + })) + } + + /// Establishes a connection to the specified `addr`. + pub async fn connect_addr(addr: SocketAddr) -> io::Result { + let family = match addr { + SocketAddr::V4(_) => IpAddressFamily::Ipv4, + SocketAddr::V6(_) => IpAddressFamily::Ipv6, + }; + let socket = TcpSocket::create(family).map_err(to_io_err)?; + + let remote_address = match addr { + SocketAddr::V4(addr) => { + let ip = addr.ip().octets(); + let address = (ip[0], ip[1], ip[2], ip[3]); + let port = addr.port(); + IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }) + } + SocketAddr::V6(_) => todo!("IPv6 not yet supported in `wstd::net::TcpStream`"), + }; + + // p3 connect is async + socket.connect(remote_address).await.map_err(to_io_err)?; + + Self::from_connected_socket(socket) + } + + /// Create a TcpStream from an already-connected socket. + pub(crate) fn from_connected_socket(socket: TcpSocket) -> io::Result { + // Get receive stream + let (recv_reader, _recv_completion) = socket.receive(); + let input = AsyncInputStream::new(recv_reader); + + // Create a send stream and wire it to the socket + let (send_writer, send_reader) = wasip3::wit_stream::new::(); + let _send_completion = socket.send(send_reader); + let output = AsyncOutputStream::new(send_writer); + + // The socket is consumed here — the streams and completion futures + // keep the underlying connection alive via their handles. + Ok(TcpStream::new(input, output)) + } + + pub fn split(&mut self) -> (ReadHalf<'_>, WriteHalf<'_>) { + let ptr = self as *mut TcpStream; + // Safety: ReadHalf only accesses input, WriteHalf only accesses output + #[allow(unsafe_code)] + unsafe { + (ReadHalf(&mut *ptr), WriteHalf(&mut *ptr)) + } + } } -} -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 + 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> { + Some(&self.input) + } } - async fn flush(&mut self) -> io::Result<()> { - self.0.flush().await + 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 + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + Some(&self.output) + } } - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - self.0.as_async_output_stream() + pub struct ReadHalf<'a>(&'a mut TcpStream); + impl<'a> io::AsyncRead for ReadHalf<'a> { + async fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.input.read(buf).await + } + + fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + Some(&self.0.input) + } } -} -impl<'a> Drop for WriteHalf<'a> { - fn drop(&mut self) { - let _ = self - .0 - .socket - .shutdown(wasip2::sockets::tcp::ShutdownType::Send); + pub struct WriteHalf<'a>(&'a mut TcpStream); + impl<'a> io::AsyncWrite for WriteHalf<'a> { + async fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.output.write(buf).await + } + + async fn flush(&mut self) -> io::Result<()> { + self.0.output.flush().await + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + Some(&self.0.output) + } } } + +#[cfg(feature = "wasip3")] +pub use p3::*; diff --git a/src/rand/mod.rs b/src/rand/mod.rs index 8474b80..d2d43f1 100644 --- a/src/rand/mod.rs +++ b/src/rand/mod.rs @@ -1,5 +1,9 @@ //! Random number generation. +#[cfg(feature = "wasip3")] +use wasip3::random; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::random; /// Fill the slice with cryptographically secure random bytes. diff --git a/src/runtime/block_on.rs b/src/runtime/block_on.rs index c7bbd31..0f98bbc 100644 --- a/src/runtime/block_on.rs +++ b/src/runtime/block_on.rs @@ -1,10 +1,14 @@ use super::{REACTOR, Reactor}; use std::future::Future; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use std::pin::pin; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use std::task::{Context, Poll, Waker}; -/// Start the event loop. Blocks until the future + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +/// Start the event loop. Blocks until the future completes. pub fn block_on(fut: F) -> F::Output where F: Future, @@ -62,3 +66,32 @@ where } } } + +// +// In WASI 0.3, async operations are natively async — there are no Pollables to +// manage. The block_on loop just drains the ready list. When no tasks are ready, +// the runtime is done (native async operations will re-schedule tasks when they +// complete). + +#[cfg(feature = "wasip3")] +/// Start the event loop. Blocks until the future completes. +/// +/// Delegates to wit-bindgen's block_on which integrates with the component +/// model's async runtime (waitable-set polling) for native p3 async support. +pub fn block_on(fut: F) -> F::Output +where + F: Future + 'static, + F::Output: 'static, +{ + // Set up the reactor for spawn support + let reactor = Reactor::new(); + let prev = REACTOR.replace(Some(reactor)); + if prev.is_some() { + panic!("cannot wstd::runtime::block_on inside an existing block_on!") + } + + let result = wit_bindgen::rt::async_support::block_on(fut); + + REACTOR.replace(None); + result +} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 24b9fc2..5ecd385 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -2,10 +2,7 @@ //! //! The way to use this is to call [`block_on()`]. Inside the future, [`Reactor::current`] //! will give an instance of the [`Reactor`] running the event loop, which can be -//! to [`AsyncPollable::wait_for`] instances of -//! [`wasip2::Pollable`](https://docs.rs/wasi/latest/wasi/io/poll/struct.Pollable.html). -//! This will automatically wait for the futures to resolve, and call the -//! necessary wakers to work. +//! used to spawn tasks and (on p2) to schedule pollables. #![deny(missing_debug_implementations, nonstandard_style)] #![warn(missing_docs, unreachable_pub)] @@ -15,10 +12,13 @@ mod reactor; pub use ::async_task::Task; pub use block_on::block_on; -pub use reactor::{AsyncPollable, Reactor, WaitFor}; +pub use reactor::Reactor; use std::cell::RefCell; -// There are no threads in WASI 0.2, so this is just a safe way to thread a single reactor to all +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use reactor::{AsyncPollable, WaitFor}; + +// There are no threads in WASI, so this is just a safe way to thread a single reactor to all // use sites in the background. std::thread_local! { pub(crate) static REACTOR: RefCell> = const { RefCell::new(None) }; diff --git a/src/runtime/reactor.rs b/src/runtime/reactor.rs index 32dc073..0669a2d 100644 --- a/src/runtime/reactor.rs +++ b/src/runtime/reactor.rs @@ -1,459 +1,568 @@ -use super::REACTOR; - -use async_task::{Runnable, Task}; -use core::future::Future; -use core::pin::Pin; -use core::task::{Context, Poll, Waker}; -use slab::Slab; -use std::collections::{HashMap, VecDeque}; -use std::sync::{Arc, Mutex}; -use wasip2::io::poll::Pollable; - -/// A key for a `Pollable`, which is an index into the `Slab` in `Reactor`. -#[repr(transparent)] -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub(crate) struct EventKey(pub(crate) usize); - -/// A Registration is a reference to the Reactor's owned Pollable. When the registration is -/// dropped, the reactor will drop the Pollable resource. -#[derive(Debug, PartialEq, Eq, Hash)] -struct Registration { - key: EventKey, -} - -impl Drop for Registration { - fn drop(&mut self) { - Reactor::current().deregister_event(self.key) +// +// The p2 reactor manages WASI Pollables and converts them into async futures. +// It maintains a Slab of Pollables, a map of waiters, and a ready list of +// runnables. + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +mod p2 { + use super::super::REACTOR; + + use async_task::{Runnable, Task}; + use core::future::Future; + use core::pin::Pin; + use core::task::{Context, Poll, Waker}; + use slab::Slab; + use std::collections::{HashMap, VecDeque}; + use std::sync::{Arc, Mutex}; + use wasip2::io::poll::Pollable; + + /// A key for a `Pollable`, which is an index into the `Slab` in `Reactor`. + #[repr(transparent)] + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] + pub(crate) struct EventKey(pub(crate) usize); + + /// A Registration is a reference to the Reactor's owned Pollable. When the registration is + /// dropped, the reactor will drop the Pollable resource. + #[derive(Debug, PartialEq, Eq, Hash)] + struct Registration { + key: EventKey, } -} -/// An AsyncPollable is a reference counted Registration. It can be cloned, and used to create -/// as many WaitFor futures on a Pollable that the user needs. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AsyncPollable(Arc); - -impl AsyncPollable { - /// Create an `AsyncPollable` from a Wasi `Pollable`. Schedules the `Pollable` with the current - /// `Reactor`. - pub fn new(pollable: Pollable) -> Self { - Reactor::current().schedule(pollable) - } - /// Create a Future that waits for the Pollable's readiness. - pub fn wait_for(&self) -> WaitFor { - use std::sync::atomic::{AtomicU64, Ordering}; - static COUNTER: AtomicU64 = AtomicU64::new(0); - let unique = COUNTER.fetch_add(1, Ordering::Relaxed); - WaitFor { - waitee: Waitee { - pollable: self.clone(), - unique, - }, - needs_deregistration: false, + impl Drop for Registration { + fn drop(&mut self) { + Reactor::current().deregister_event(self.key) } } -} -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -struct Waitee { - /// This needs to be a reference counted registration, because it may outlive the AsyncPollable - /// &self that it was created from. - pollable: AsyncPollable, - unique: u64, -} + /// An AsyncPollable is a reference counted Registration. It can be cloned, and used to create + /// as many WaitFor futures on a Pollable that the user needs. + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct AsyncPollable(Arc); -/// A Future that waits for the Pollable's readiness. -#[must_use = "futures do nothing unless polled or .awaited"] -#[derive(Debug)] -pub struct WaitFor { - waitee: Waitee, - needs_deregistration: bool, -} -impl Future for WaitFor { - type Output = (); - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let reactor = Reactor::current(); - if reactor.ready(&self.as_ref().waitee, cx.waker()) { - Poll::Ready(()) - } else { - self.as_mut().needs_deregistration = true; - Poll::Pending + impl AsyncPollable { + /// Create an `AsyncPollable` from a Wasi `Pollable`. Schedules the `Pollable` with the current + /// `Reactor`. + pub fn new(pollable: Pollable) -> Self { + Reactor::current().schedule(pollable) } - } -} -impl Drop for WaitFor { - fn drop(&mut self) { - if self.needs_deregistration { - Reactor::current().deregister_waitee(&self.waitee) + /// Create a Future that waits for the Pollable's readiness. + pub fn wait_for(&self) -> WaitFor { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let unique = COUNTER.fetch_add(1, Ordering::Relaxed); + WaitFor { + waitee: Waitee { + pollable: self.clone(), + unique, + }, + needs_deregistration: false, + } } } -} - -/// Manage async system resources for WASI 0.2 -#[derive(Debug, Clone)] -pub struct Reactor { - inner: Arc, -} - -/// The private, internal `Reactor` implementation - factored out so we can take -/// a lock of the whole. -#[derive(Debug)] -struct InnerReactor { - pollables: Mutex>, - wakers: Mutex>, - ready_list: Mutex>, -} -impl Reactor { - /// Return a `Reactor` for the currently running `wstd::runtime::block_on`. - /// - /// # Panic - /// This will panic if called outside of `wstd::runtime::block_on`. - pub fn current() -> Self { - REACTOR.with(|r| { - r.borrow() - .as_ref() - .expect("Reactor::current must be called within a wstd runtime") - .clone() - }) + #[derive(Debug, PartialEq, Eq, Hash, Clone)] + struct Waitee { + /// This needs to be a reference counted registration, because it may outlive the AsyncPollable + /// &self that it was created from. + pollable: AsyncPollable, + unique: u64, } - /// Create a new instance of `Reactor` - pub(crate) fn new() -> Self { - Self { - inner: Arc::new(InnerReactor { - pollables: Mutex::new(Slab::new()), - wakers: Mutex::new(HashMap::new()), - ready_list: Mutex::new(VecDeque::new()), - }), + /// A Future that waits for the Pollable's readiness. + #[must_use = "futures do nothing unless polled or .awaited"] + #[derive(Debug)] + pub struct WaitFor { + waitee: Waitee, + needs_deregistration: bool, + } + impl Future for WaitFor { + type Output = (); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let reactor = Reactor::current(); + if reactor.ready(&self.as_ref().waitee, cx.waker()) { + Poll::Ready(()) + } else { + self.as_mut().needs_deregistration = true; + Poll::Pending + } + } + } + impl Drop for WaitFor { + fn drop(&mut self) { + if self.needs_deregistration { + Reactor::current().deregister_waitee(&self.waitee) + } } } - /// The reactor tracks the set of WASI pollables which have an associated - /// Future pending on their readiness. This function returns indicating - /// that set of pollables is not empty. - pub(crate) fn pending_pollables_is_empty(&self) -> bool { - self.inner.wakers.lock().unwrap().is_empty() + /// Manage async system resources for WASI 0.2 + #[derive(Debug, Clone)] + pub struct Reactor { + inner: Arc, } - /// Block until at least one pending pollable is ready, waking a pending future. - /// Precondition: self.nonempty_pending_pollables() is true. - pub(crate) fn block_on_pollables(&self) { - self.check_pollables(|targets| { - debug_assert_ne!( - targets.len(), - 0, - "Attempting to block on an empty list of pollables - without any pending work, no progress can be made and wasip2::io::poll::poll will trap" - ); - wasip2::io::poll::poll(targets) - - }) + /// The private, internal `Reactor` implementation - factored out so we can take + /// a lock of the whole. + #[derive(Debug)] + struct InnerReactor { + pollables: Mutex>, + wakers: Mutex>, + ready_list: Mutex>, } - /// Without blocking, check for any ready pollables and wake the - /// associated futures. - pub(crate) fn nonblock_check_pollables(&self) { - // If there are no pollables with associated pending futures, there is - // no work to do here, so return immediately. - if self.pending_pollables_is_empty() { - return; + impl Reactor { + /// Return a `Reactor` for the currently running `wstd::runtime::block_on`. + /// + /// # Panic + /// This will panic if called outside of `wstd::runtime::block_on`. + pub fn current() -> Self { + REACTOR.with(|r| { + r.borrow() + .as_ref() + .expect("Reactor::current must be called within a wstd runtime") + .clone() + }) } - // Lazily create a pollable which always resolves to ready. - use std::sync::LazyLock; - static READY_POLLABLE: LazyLock = - LazyLock::new(|| wasip2::clocks::monotonic_clock::subscribe_duration(0)); - - self.check_pollables(|targets| { - // Create a new set of targets, with the addition of the ready - // pollable: - let ready_index = targets.len(); - let mut new_targets = Vec::with_capacity(ready_index + 1); - new_targets.extend_from_slice(targets); - new_targets.push(&*READY_POLLABLE); - - // Poll is now guaranteed to return immediately, because at least - // one member is ready: - let mut ready_list = wasip2::io::poll::poll(&new_targets); - - // Erase our extra ready pollable from the ready list: - ready_list.retain(|e| *e != ready_index as u32); - ready_list - }) - } - /// Common core of blocking and nonblocking pollable checks. Wakes any - /// futures which are pending on the pollables, according to the result of - /// the check_ready function. - /// Precondition: self.nonempty_pending_pollables() is true. - fn check_pollables(&self, check_ready: F) - where - F: FnOnce(&[&Pollable]) -> Vec, - { - let wakers = self.inner.wakers.lock().unwrap(); - let pollables = self.inner.pollables.lock().unwrap(); - - // We're about to wait for a number of pollables. When they wake we get - // the *indexes* back for the pollables whose events were available - so - // we need to be able to associate the index with the right waker. - - // We start by iterating over the pollables, and keeping note of which - // pollable belongs to which waker - let mut indexed_wakers = Vec::with_capacity(wakers.len()); - let mut targets = Vec::with_capacity(wakers.len()); - for (waitee, waker) in wakers.iter() { - let pollable_index = waitee.pollable.0.key; - indexed_wakers.push(waker); - targets.push(&pollables[pollable_index.0]); + /// Create a new instance of `Reactor` + pub(crate) fn new() -> Self { + Self { + inner: Arc::new(InnerReactor { + pollables: Mutex::new(Slab::new()), + wakers: Mutex::new(HashMap::new()), + ready_list: Mutex::new(VecDeque::new()), + }), + } } - // Now that we have that association, we're ready to check our targets for readiness. - // (This is either a wasi poll, or the nonblocking variant.) - let ready_indexes = check_ready(&targets); + /// The reactor tracks the set of WASI pollables which have an associated + /// Future pending on their readiness. This function returns indicating + /// that set of pollables is not empty. + pub(crate) fn pending_pollables_is_empty(&self) -> bool { + self.inner.wakers.lock().unwrap().is_empty() + } - // Once we have the indexes for which pollables are available, we need - // to convert it back to the right keys for the wakers. Earlier we - // established a positional index -> waker key relationship, so we can - // go right ahead and perform a lookup there. - let ready_wakers = ready_indexes - .into_iter() - .map(|index| indexed_wakers[index as usize]); + /// Block until at least one pending pollable is ready, waking a pending future. + /// Precondition: self.nonempty_pending_pollables() is true. + pub(crate) fn block_on_pollables(&self) { + self.check_pollables(|targets| { + debug_assert_ne!( + targets.len(), + 0, + "Attempting to block on an empty list of pollables - without any pending work, no progress can be made and wasip2::io::poll::poll will trap" + ); + wasip2::io::poll::poll(targets) + + }) + } - for waker in ready_wakers { - waker.wake_by_ref() + /// Without blocking, check for any ready pollables and wake the + /// associated futures. + pub(crate) fn nonblock_check_pollables(&self) { + // If there are no pollables with associated pending futures, there is + // no work to do here, so return immediately. + if self.pending_pollables_is_empty() { + return; + } + // Lazily create a pollable which always resolves to ready. + use std::sync::LazyLock; + static READY_POLLABLE: LazyLock = + LazyLock::new(|| wasip2::clocks::monotonic_clock::subscribe_duration(0)); + + self.check_pollables(|targets| { + // Create a new set of targets, with the addition of the ready + // pollable: + let ready_index = targets.len(); + let mut new_targets = Vec::with_capacity(ready_index + 1); + new_targets.extend_from_slice(targets); + new_targets.push(&*READY_POLLABLE); + + // Poll is now guaranteed to return immediately, because at least + // one member is ready: + let mut ready_list = wasip2::io::poll::poll(&new_targets); + + // Erase our extra ready pollable from the ready list: + ready_list.retain(|e| *e != ready_index as u32); + ready_list + }) } - } - /// Turn a Wasi [`Pollable`] into an [`AsyncPollable`] - pub fn schedule(&self, pollable: Pollable) -> AsyncPollable { - let mut pollables = self.inner.pollables.lock().unwrap(); - let key = EventKey(pollables.insert(pollable)); - AsyncPollable(Arc::new(Registration { key })) - } + /// Common core of blocking and nonblocking pollable checks. Wakes any + /// futures which are pending on the pollables, according to the result of + /// the check_ready function. + /// Precondition: self.nonempty_pending_pollables() is true. + fn check_pollables(&self, check_ready: F) + where + F: FnOnce(&[&Pollable]) -> Vec, + { + let wakers = self.inner.wakers.lock().unwrap(); + let pollables = self.inner.pollables.lock().unwrap(); + + // We're about to wait for a number of pollables. When they wake we get + // the *indexes* back for the pollables whose events were available - so + // we need to be able to associate the index with the right waker. + + // We start by iterating over the pollables, and keeping note of which + // pollable belongs to which waker + let mut indexed_wakers = Vec::with_capacity(wakers.len()); + let mut targets = Vec::with_capacity(wakers.len()); + for (waitee, waker) in wakers.iter() { + let pollable_index = waitee.pollable.0.key; + indexed_wakers.push(waker); + targets.push(&pollables[pollable_index.0]); + } + + // Now that we have that association, we're ready to check our targets for readiness. + // (This is either a wasi poll, or the nonblocking variant.) + let ready_indexes = check_ready(&targets); + + // Once we have the indexes for which pollables are available, we need + // to convert it back to the right keys for the wakers. Earlier we + // established a positional index -> waker key relationship, so we can + // go right ahead and perform a lookup there. + let ready_wakers = ready_indexes + .into_iter() + .map(|index| indexed_wakers[index as usize]); + + for waker in ready_wakers { + waker.wake_by_ref() + } + } - fn deregister_event(&self, key: EventKey) { - let mut pollables = self.inner.pollables.lock().unwrap(); - pollables.remove(key.0); - } + /// Turn a Wasi [`Pollable`] into an [`AsyncPollable`] + pub fn schedule(&self, pollable: Pollable) -> AsyncPollable { + let mut pollables = self.inner.pollables.lock().unwrap(); + let key = EventKey(pollables.insert(pollable)); + AsyncPollable(Arc::new(Registration { key })) + } - fn deregister_waitee(&self, waitee: &Waitee) { - let mut wakers = self.inner.wakers.lock().unwrap(); - wakers.remove(waitee); - } + fn deregister_event(&self, key: EventKey) { + let mut pollables = self.inner.pollables.lock().unwrap(); + pollables.remove(key.0); + } - fn ready(&self, waitee: &Waitee, waker: &Waker) -> bool { - let ready = self - .inner - .pollables - .lock() - .unwrap() - .get(waitee.pollable.0.key.0) - .expect("only live EventKey can be checked for readiness") - .ready(); - if !ready { - self.inner - .wakers + fn deregister_waitee(&self, waitee: &Waitee) { + let mut wakers = self.inner.wakers.lock().unwrap(); + wakers.remove(waitee); + } + + fn ready(&self, waitee: &Waitee, waker: &Waker) -> bool { + let ready = self + .inner + .pollables .lock() .unwrap() - .insert(waitee.clone(), waker.clone()); + .get(waitee.pollable.0.key.0) + .expect("only live EventKey can be checked for readiness") + .ready(); + if !ready { + self.inner + .wakers + .lock() + .unwrap() + .insert(waitee.clone(), waker.clone()); + } + ready } - ready - } - /// We need an unchecked spawn for implementing `block_on`, where the - /// implementation does not expose the resulting Task and does not return - /// until all tasks have finished execution. - /// - /// # Safety - /// Caller must ensure that the Task does not outlive the Future F or - /// F::Output T. - #[allow(unsafe_code)] - pub(crate) unsafe fn spawn_unchecked(&self, fut: F) -> Task - where - F: Future, - { - let this = self.clone(); - let schedule = move |runnable| this.inner.ready_list.lock().unwrap().push_back(runnable); - - // SAFETY: - // we're using this exactly like async_task::spawn_local, except that - // the schedule function is not Send or Sync, because Runnable is not - // Send or Sync. This is safe because wasm32-wasip2 is always - // single-threaded. + /// We need an unchecked spawn for implementing `block_on`, where the + /// implementation does not expose the resulting Task and does not return + /// until all tasks have finished execution. + /// + /// # Safety + /// Caller must ensure that the Task does not outlive the Future F or + /// F::Output T. #[allow(unsafe_code)] - let (runnable, task) = unsafe { async_task::spawn_unchecked(fut, schedule) }; - self.inner.ready_list.lock().unwrap().push_back(runnable); - task - } + pub(crate) unsafe fn spawn_unchecked(&self, fut: F) -> Task + where + F: Future, + { + let this = self.clone(); + let schedule = + move |runnable| this.inner.ready_list.lock().unwrap().push_back(runnable); + + // SAFETY: + // we're using this exactly like async_task::spawn_local, except that + // the schedule function is not Send or Sync, because Runnable is not + // Send or Sync. This is safe because wasm32-wasip2 is always + // single-threaded. + #[allow(unsafe_code)] + let (runnable, task) = unsafe { async_task::spawn_unchecked(fut, schedule) }; + self.inner.ready_list.lock().unwrap().push_back(runnable); + task + } - /// Spawn a `Task` on the `Reactor`. - pub fn spawn(&self, fut: F) -> Task - where - F: Future + 'static, - T: 'static, - { - // Safety: 'static constraints satisfy the lifetime requirements - #[allow(unsafe_code)] - unsafe { - self.spawn_unchecked(fut) + /// Spawn a `Task` on the `Reactor`. + pub fn spawn(&self, fut: F) -> Task + where + F: Future + 'static, + T: 'static, + { + // Safety: 'static constraints satisfy the lifetime requirements + #[allow(unsafe_code)] + unsafe { + self.spawn_unchecked(fut) + } + } + + pub(crate) fn pop_ready_list(&self) -> Option { + self.inner.ready_list.lock().unwrap().pop_front() } - } - pub(super) fn pop_ready_list(&self) -> Option { - self.inner.ready_list.lock().unwrap().pop_front() + pub(crate) fn ready_list_is_empty(&self) -> bool { + self.inner.ready_list.lock().unwrap().is_empty() + } } - pub(super) fn ready_list_is_empty(&self) -> bool { - self.inner.ready_list.lock().unwrap().is_empty() + #[cfg(test)] + mod test { + use super::*; + // Using WASMTIME_LOG, observe that this test doesn't even call poll() - the pollable is ready + // immediately. + #[test] + fn subscribe_no_duration() { + crate::runtime::block_on(async { + let reactor = Reactor::current(); + let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(0); + let sched = reactor.schedule(pollable); + sched.wait_for().await; + }) + } + // Using WASMTIME_LOG, observe that this test calls poll() until the timer is ready. + #[test] + fn subscribe_some_duration() { + crate::runtime::block_on(async { + let reactor = Reactor::current(); + let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); + let sched = reactor.schedule(pollable); + sched.wait_for().await; + }) + } + + // Using WASMTIME_LOG, observe that this test results in a single poll() on the second + // subscription, rather than spinning in poll() with first subscription, which is instantly + // ready, but not what the waker requests. + #[test] + fn subscribe_multiple_durations() { + crate::runtime::block_on(async { + let reactor = Reactor::current(); + let now = wasip2::clocks::monotonic_clock::subscribe_duration(0); + let soon = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); + let now = reactor.schedule(now); + let soon = reactor.schedule(soon); + soon.wait_for().await; + drop(now) + }) + } + + // Using WASMTIME_LOG, observe that this test results in two calls to poll(), one with both + // pollables because both are awaiting, and one with just the later pollable. + #[test] + fn subscribe_multiple_durations_zipped() { + crate::runtime::block_on(async { + let reactor = Reactor::current(); + let start = wasip2::clocks::monotonic_clock::now(); + let soon = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); + let later = wasip2::clocks::monotonic_clock::subscribe_duration(40_000_000); + let soon = reactor.schedule(soon); + let later = reactor.schedule(later); + + futures_lite::future::zip( + async move { + soon.wait_for().await; + println!( + "*** subscribe_duration(soon) ready ({})", + wasip2::clocks::monotonic_clock::now() - start + ); + }, + async move { + later.wait_for().await; + println!( + "*** subscribe_duration(later) ready ({})", + wasip2::clocks::monotonic_clock::now() - start + ); + }, + ) + .await; + }) + } + + #[test] + fn progresses_wasi_independent_futures() { + crate::runtime::block_on(async { + let start = wasip2::clocks::monotonic_clock::now(); + + let reactor = Reactor::current(); + const LONG_DURATION: u64 = 1_000_000_000; + let later = wasip2::clocks::monotonic_clock::subscribe_duration(LONG_DURATION); + let later = reactor.schedule(later); + let mut polled_before = false; + // This is basically futures_lite::future::yield_now, except with a boolean + // `polled_before` so we can definitively observe what happened + let wasi_independent_future = futures_lite::future::poll_fn(|cx| { + if polled_before { + std::task::Poll::Ready(true) + } else { + polled_before = true; + cx.waker().wake_by_ref(); + std::task::Poll::Pending + } + }); + let later = async { + later.wait_for().await; + false + }; + let wasi_independent_future_won = + futures_lite::future::race(wasi_independent_future, later).await; + assert!( + wasi_independent_future_won, + "wasi_independent_future should win the race" + ); + const SHORT_DURATION: u64 = LONG_DURATION / 100; + let soon = wasip2::clocks::monotonic_clock::subscribe_duration(SHORT_DURATION); + let soon = reactor.schedule(soon); + soon.wait_for().await; + + let end = wasip2::clocks::monotonic_clock::now(); + + let duration = end - start; + assert!( + duration > SHORT_DURATION, + "{duration} greater than short duration shows awaited for `soon` properly" + ); + // Upper bound is high enough that even the very poor windows CI machines meet it + assert!( + duration < (5 * SHORT_DURATION), + "{duration} less than a reasonable multiple of short duration {SHORT_DURATION} shows did not await for `later`" + ); + }) + } + + #[test] + fn cooperative_concurrency() { + crate::runtime::block_on(async { + let cpu_heavy = async move { + // Simulating a CPU-heavy task that runs for 1 second and yields occasionally + for _ in 0..10 { + std::thread::sleep(std::time::Duration::from_millis(100)); + futures_lite::future::yield_now().await; + } + true + }; + let timeout = async move { + crate::time::Timer::after(crate::time::Duration::from_millis(200)) + .wait() + .await; + false + }; + let mut future_group = futures_concurrency::future::FutureGroup::< + Pin>>, + >::new(); + future_group.insert(Box::pin(cpu_heavy)); + future_group.insert(Box::pin(timeout)); + let result = futures_lite::StreamExt::next(&mut future_group).await; + assert_eq!(result, Some(false), "cpu_heavy task should have timed out"); + }); + } } } -#[cfg(test)] -mod test { - use super::*; - // Using WASMTIME_LOG, observe that this test doesn't even call poll() - the pollable is ready - // immediately. - #[test] - fn subscribe_no_duration() { - crate::runtime::block_on(async { - let reactor = Reactor::current(); - let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(0); - let sched = reactor.schedule(pollable); - sched.wait_for().await; - }) - } - // Using WASMTIME_LOG, observe that this test calls poll() until the timer is ready. - #[test] - fn subscribe_some_duration() { - crate::runtime::block_on(async { - let reactor = Reactor::current(); - let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); - let sched = reactor.schedule(pollable); - sched.wait_for().await; - }) - } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +pub use p2::*; - // Using WASMTIME_LOG, observe that this test results in a single poll() on the second - // subscription, rather than spinning in poll() with first subscription, which is instantly - // ready, but not what the waker requests. - #[test] - fn subscribe_multiple_durations() { - crate::runtime::block_on(async { - let reactor = Reactor::current(); - let now = wasip2::clocks::monotonic_clock::subscribe_duration(0); - let soon = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); - let now = reactor.schedule(now); - let soon = reactor.schedule(soon); - soon.wait_for().await; - drop(now) - }) +// +// The p3 reactor is a simplified task scheduler. WASI 0.3 provides native async +// support, so there is no need to manage Pollables. The reactor only manages a +// ready list of runnables for cooperative multitasking. + +#[cfg(feature = "wasip3")] +mod p3 { + use super::super::REACTOR; + + use async_task::{Runnable, Task}; + use core::future::Future; + use std::collections::VecDeque; + use std::sync::{Arc, Mutex}; + + /// Manage async task scheduling for WASI 0.3 + #[derive(Debug, Clone)] + pub struct Reactor { + inner: Arc, } - // Using WASMTIME_LOG, observe that this test results in two calls to poll(), one with both - // pollables because both are awaiting, and one with just the later pollable. - #[test] - fn subscribe_multiple_durations_zipped() { - crate::runtime::block_on(async { - let reactor = Reactor::current(); - let start = wasip2::clocks::monotonic_clock::now(); - let soon = wasip2::clocks::monotonic_clock::subscribe_duration(10_000_000); - let later = wasip2::clocks::monotonic_clock::subscribe_duration(40_000_000); - let soon = reactor.schedule(soon); - let later = reactor.schedule(later); - - futures_lite::future::zip( - async move { - soon.wait_for().await; - println!( - "*** subscribe_duration(soon) ready ({})", - wasip2::clocks::monotonic_clock::now() - start - ); - }, - async move { - later.wait_for().await; - println!( - "*** subscribe_duration(later) ready ({})", - wasip2::clocks::monotonic_clock::now() - start - ); - }, - ) - .await; - }) + #[derive(Debug)] + struct InnerReactor { + ready_list: Mutex>, } - #[test] - fn progresses_wasi_independent_futures() { - crate::runtime::block_on(async { - let start = wasip2::clocks::monotonic_clock::now(); + impl Reactor { + /// Return a `Reactor` for the currently running `wstd::runtime::block_on`. + /// + /// # Panic + /// This will panic if called outside of `wstd::runtime::block_on`. + pub fn current() -> Self { + REACTOR.with(|r| { + r.borrow() + .as_ref() + .expect("Reactor::current must be called within a wstd runtime") + .clone() + }) + } - let reactor = Reactor::current(); - const LONG_DURATION: u64 = 1_000_000_000; - let later = wasip2::clocks::monotonic_clock::subscribe_duration(LONG_DURATION); - let later = reactor.schedule(later); - let mut polled_before = false; - // This is basically futures_lite::future::yield_now, except with a boolean - // `polled_before` so we can definitively observe what happened - let wasi_independent_future = futures_lite::future::poll_fn(|cx| { - if polled_before { - std::task::Poll::Ready(true) - } else { - polled_before = true; - cx.waker().wake_by_ref(); - std::task::Poll::Pending - } - }); - let later = async { - later.wait_for().await; - false - }; - let wasi_independent_future_won = - futures_lite::future::race(wasi_independent_future, later).await; - assert!( - wasi_independent_future_won, - "wasi_independent_future should win the race" - ); - const SHORT_DURATION: u64 = LONG_DURATION / 100; - let soon = wasip2::clocks::monotonic_clock::subscribe_duration(SHORT_DURATION); - let soon = reactor.schedule(soon); - soon.wait_for().await; - - let end = wasip2::clocks::monotonic_clock::now(); - - let duration = end - start; - assert!( - duration > SHORT_DURATION, - "{duration} greater than short duration shows awaited for `soon` properly" - ); - // Upper bound is high enough that even the very poor windows CI machines meet it - assert!( - duration < (5 * SHORT_DURATION), - "{duration} less than a reasonable multiple of short duration {SHORT_DURATION} shows did not await for `later`" - ); - }) - } + /// Create a new instance of `Reactor` + pub(crate) fn new() -> Self { + Self { + inner: Arc::new(InnerReactor { + ready_list: Mutex::new(VecDeque::new()), + }), + } + } - #[test] - fn cooperative_concurrency() { - crate::runtime::block_on(async { - let cpu_heavy = async move { - // Simulating a CPU-heavy task that runs for 1 second and yields occasionally - for _ in 0..10 { - std::thread::sleep(std::time::Duration::from_millis(100)); - futures_lite::future::yield_now().await; - } - true - }; - let timeout = async move { - crate::time::Timer::after(crate::time::Duration::from_millis(200)) - .wait() - .await; - false - }; - let mut future_group = futures_concurrency::future::FutureGroup::< - Pin>>, - >::new(); - future_group.insert(Box::pin(cpu_heavy)); - future_group.insert(Box::pin(timeout)); - let result = futures_lite::StreamExt::next(&mut future_group).await; - assert_eq!(result, Some(false), "cpu_heavy task should have timed out"); - }); + /// We need an unchecked spawn for implementing `block_on`, where the + /// implementation does not expose the resulting Task and does not return + /// until all tasks have finished execution. + /// + /// # Safety + /// Caller must ensure that the Task does not outlive the Future F or + /// F::Output T. + #[allow(unsafe_code)] + pub(crate) unsafe fn spawn_unchecked(&self, fut: F) -> Task + where + F: Future, + { + let this = self.clone(); + let schedule = + move |runnable| this.inner.ready_list.lock().unwrap().push_back(runnable); + + #[allow(unsafe_code)] + let (runnable, task) = unsafe { async_task::spawn_unchecked(fut, schedule) }; + self.inner.ready_list.lock().unwrap().push_back(runnable); + task + } + + /// Spawn a `Task` on the `Reactor`. + pub fn spawn(&self, fut: F) -> Task + where + F: Future + 'static, + T: 'static, + { + #[allow(unsafe_code)] + unsafe { + self.spawn_unchecked(fut) + } + } + + #[allow(dead_code)] + pub(crate) fn pop_ready_list(&self) -> Option { + self.inner.ready_list.lock().unwrap().pop_front() + } + + #[allow(dead_code)] + pub(crate) fn ready_list_is_empty(&self) -> bool { + self.inner.ready_list.lock().unwrap().is_empty() + } } } + +#[cfg(feature = "wasip3")] +pub use p3::*; diff --git a/src/time/duration.rs b/src/time/duration.rs index 7f67ceb..c63a6a8 100644 --- a/src/time/duration.rs +++ b/src/time/duration.rs @@ -1,6 +1,11 @@ use super::{Instant, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; + +#[cfg(feature = "wasip3")] +use wasip3::clocks::monotonic_clock; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::clocks::monotonic_clock; /// A Duration type to represent a span of time, typically used for system diff --git a/src/time/instant.rs b/src/time/instant.rs index 6e9cf97..be807c9 100644 --- a/src/time/instant.rs +++ b/src/time/instant.rs @@ -1,6 +1,11 @@ use super::{Duration, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; + +#[cfg(feature = "wasip3")] +use wasip3::clocks::monotonic_clock; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::clocks::monotonic_clock; /// A measurement of a monotonically nondecreasing clock. Opaque and useful only @@ -10,8 +15,13 @@ use wasip2::clocks::monotonic_clock; /// without coherence issues, just like if we were implementing this in the /// stdlib. #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pub struct Instant(pub(crate) monotonic_clock::Instant); +#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] +#[cfg(feature = "wasip3")] +pub struct Instant(pub(crate) monotonic_clock::Mark); + impl Instant { /// Returns an instant corresponding to "now". /// @@ -24,7 +34,7 @@ impl Instant { /// ``` #[must_use] pub fn now() -> Self { - Instant(wasip2::clocks::monotonic_clock::now()) + Instant(monotonic_clock::now()) } /// Returns the amount of time elapsed from another instant to this one, or zero duration if diff --git a/src/time/mod.rs b/src/time/mod.rs index db0e1b3..a2d5017 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -7,33 +7,44 @@ mod instant; pub use duration::Duration; pub use instant::Instant; -use pin_project_lite::pin_project; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; + +use crate::iter::AsyncIterator; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] use wasip2::clocks::{ monotonic_clock::{subscribe_duration, subscribe_instant}, wall_clock, }; -use crate::{ - iter::AsyncIterator, - runtime::{AsyncPollable, Reactor}, -}; +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use crate::runtime::{AsyncPollable, Reactor}; + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +use pin_project_lite::pin_project; + +#[cfg(feature = "wasip3")] +use wasip3::clocks::{monotonic_clock, system_clock}; + /// A measurement of the system clock, useful for talking to external entities /// like the file system or other processes. May be converted losslessly to a /// more useful `std::time::SystemTime` to provide more methods. +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug, Clone, Copy)] #[allow(dead_code)] pub struct SystemTime(wall_clock::Datetime); +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl SystemTime { pub fn now() -> Self { Self(wall_clock::now()) } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl From for std::time::SystemTime { fn from(st: SystemTime) -> Self { std::time::SystemTime::UNIX_EPOCH @@ -42,6 +53,35 @@ impl From for std::time::SystemTime { } } +#[cfg(feature = "wasip3")] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct SystemTime(system_clock::Instant); + +#[cfg(feature = "wasip3")] +impl SystemTime { + pub fn now() -> Self { + Self(system_clock::now()) + } +} + +#[cfg(feature = "wasip3")] +impl From for std::time::SystemTime { + fn from(st: SystemTime) -> Self { + // p3 system_clock::Instant has i64 seconds + if st.0.seconds >= 0 { + std::time::SystemTime::UNIX_EPOCH + + std::time::Duration::from_secs(st.0.seconds as u64) + + std::time::Duration::from_nanos(st.0.nanoseconds.into()) + } else { + std::time::SystemTime::UNIX_EPOCH + - std::time::Duration::from_secs((-st.0.seconds) as u64) + + std::time::Duration::from_nanos(st.0.nanoseconds.into()) + } + } +} + + /// An async iterator representing notifications at fixed interval. pub fn interval(duration: Duration) -> Interval { Interval { duration } @@ -62,9 +102,12 @@ impl AsyncIterator for Interval { } } + +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] #[derive(Debug)] pub struct Timer(Option); +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl Timer { pub fn never() -> Timer { Timer(None) @@ -86,6 +129,7 @@ impl Timer { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] pin_project! { /// Future created by [`Timer::wait`] #[must_use = "futures do nothing unless polled or .awaited"] @@ -95,6 +139,7 @@ pin_project! { } } +#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] impl Future for Wait { type Output = Instant; @@ -110,6 +155,75 @@ impl Future for Wait { } } + +#[cfg(feature = "wasip3")] +pub struct Timer { + kind: TimerKind, +} + +#[cfg(feature = "wasip3")] +enum TimerKind { + Never, + After(Duration), + At(Instant), +} + +#[cfg(feature = "wasip3")] +impl std::fmt::Debug for Timer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Timer").finish() + } +} + +#[cfg(feature = "wasip3")] +impl Timer { + pub fn never() -> Timer { + Timer { + kind: TimerKind::Never, + } + } + pub fn at(deadline: Instant) -> Timer { + Timer { + kind: TimerKind::At(deadline), + } + } + pub fn after(duration: Duration) -> Timer { + Timer { + kind: TimerKind::After(duration), + } + } + pub fn set_after(&mut self, duration: Duration) { + *self = Self::after(duration); + } + pub fn wait(&self) -> Wait { + let inner: Pin>> = match self.kind { + TimerKind::Never => Box::pin(std::future::pending()), + TimerKind::After(d) => Box::pin(monotonic_clock::wait_for(d.0)), + TimerKind::At(deadline) => Box::pin(monotonic_clock::wait_until(deadline.0)), + }; + Wait { inner } + } +} + +#[cfg(feature = "wasip3")] +#[must_use = "futures do nothing unless polled or .awaited"] +pub struct Wait { + inner: Pin>>, +} + +#[cfg(feature = "wasip3")] +impl Future for Wait { + type Output = Instant; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.inner.as_mut().poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(()) => Poll::Ready(Instant::now()), + } + } +} + + #[cfg(test)] mod test { use super::*; diff --git a/tests/sleep.rs b/tests/sleep.rs index eb55ea0..fe8c1ab 100644 --- a/tests/sleep.rs +++ b/tests/sleep.rs @@ -1,9 +1,26 @@ use std::error::Error; use wstd::task::sleep; use wstd::time::Duration; +use wstd::time::Instant; #[wstd::test] async fn just_sleep() -> Result<(), Box> { sleep(Duration::from_secs(1)).await; Ok(()) } + +#[wstd::test] +async fn sleep_elapsed() -> Result<(), Box> { + let start = Instant::now(); + sleep(Duration::from_millis(100)).await; + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(80), + "sleep: elapsed {elapsed:?} should be >= 80ms" + ); + assert!( + elapsed < Duration::from_secs(2), + "sleep: elapsed {elapsed:?} should be < 2s" + ); + Ok(()) +} diff --git a/tests/stdio.rs b/tests/stdio.rs new file mode 100644 index 0000000..a87ae3c --- /dev/null +++ b/tests/stdio.rs @@ -0,0 +1,18 @@ +use std::error::Error; +use wstd::io::{AsyncWrite, stderr, stdout}; + +#[wstd::test] +async fn write_stdout() -> Result<(), Box> { + let mut out = stdout(); + out.write_all(b"hello from stdout\n").await?; + out.flush().await?; + Ok(()) +} + +#[wstd::test] +async fn write_stderr() -> Result<(), Box> { + let mut err = stderr(); + err.write_all(b"hello from stderr\n").await?; + err.flush().await?; + Ok(()) +} diff --git a/tests/timer.rs b/tests/timer.rs new file mode 100644 index 0000000..fa3d692 --- /dev/null +++ b/tests/timer.rs @@ -0,0 +1,35 @@ +use std::error::Error; +use wstd::time::{Duration, Instant, Timer}; + +#[wstd::test] +async fn timer_after() -> Result<(), Box> { + let start = Instant::now(); + Timer::after(Duration::from_millis(50)).wait().await; + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(40), + "timer_after: elapsed {elapsed:?} should be >= 40ms" + ); + Ok(()) +} + +#[wstd::test] +async fn timer_at() -> Result<(), Box> { + let start = Instant::now(); + let deadline = start + Duration::from_millis(50); + Timer::at(deadline).wait().await; + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(40), + "timer_at: elapsed {elapsed:?} should be >= 40ms" + ); + Ok(()) +} + +#[wstd::test] +async fn instant_monotonic() -> Result<(), Box> { + let a = Instant::now(); + let b = Instant::now(); + assert!(b >= a, "monotonic clock should not go backwards"); + Ok(()) +} From 96ba771f855290f3e0390b9e168465919bc70c4c Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Tue, 7 Apr 2026 12:15:24 -0400 Subject: [PATCH 2/2] fix(wasip3): Target-conditional deps Now when wasm32-wasip3 lands, users can compile with just `--target wasm32-wasip3` The cfg alias activates the p3 path AND the wasip3 deps are auto-pulled. --- .github/workflows/ci.yaml | 2 +- Cargo.toml | 9 ++++++ build.rs | 13 ++++++++ src/http/body.rs | 65 +++++++++++++++++++-------------------- src/http/client.rs | 10 +++--- src/http/error.rs | 4 +-- src/http/fields.rs | 8 ++--- src/http/method.rs | 4 +-- src/http/mod.rs | 11 ++++--- src/http/request.rs | 18 +++++------ src/http/response.rs | 12 +++----- src/http/scheme.rs | 4 +-- src/http/server.rs | 9 +++--- src/io/copy.rs | 2 +- src/io/mod.rs | 2 +- src/io/stdio.rs | 20 ++++++------ src/io/streams.rs | 8 ++--- src/lib.rs | 9 ++++-- src/net/mod.rs | 4 +-- src/net/tcp_listener.rs | 8 ++--- src/net/tcp_stream.rs | 8 ++--- src/rand/mod.rs | 4 +-- src/runtime/block_on.rs | 9 +++--- src/runtime/mod.rs | 2 +- src/runtime/reactor.rs | 8 ++--- src/time/duration.rs | 4 +-- src/time/instant.rs | 8 ++--- src/time/mod.rs | 45 ++++++++++++--------------- 28 files changed, 163 insertions(+), 147 deletions(-) create mode 100644 build.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b304e81..52f956a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,7 +55,7 @@ jobs: run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 -- --nocapture - name: wstd tests (wasip3) - run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 --no-default-features --features wasip3 -- --nocapture + run: cargo test -p wstd -p wstd-axum --target wasm32-wasip2 --no-default-features --features wasip3,json -- --nocapture - name: test-programs tests run: cargo test -p test-programs -- --nocapture diff --git a/Cargo.toml b/Cargo.toml index 738b057..a0625a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ json = ["dep:serde", "dep:serde_json"] wasip2 = ["dep:wasip2"] wasip3 = ["dep:wasip3", "dep:wit-bindgen"] +[build-dependencies] +cfg_aliases = "0.2" + [dependencies] anyhow.workspace = true async-task.workspace = true @@ -38,6 +41,12 @@ wstd-macro.workspace = true serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } +# Auto-pull wasip3 deps when targeting wasm32-wasip3 so users don't need +# to also pass --features wasip3. +[target.'cfg(all(target_os = "wasi", target_env = "p3"))'.dependencies] +wasip3 = { workspace = true } +wit-bindgen = { workspace = true } + [dev-dependencies] anyhow.workspace = true clap.workspace = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..bec5023 --- /dev/null +++ b/build.rs @@ -0,0 +1,13 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + cfg_aliases! { + // True when targeting a wasip3 component, either via the explicit + // `wasip3` feature or the `wasm32-wasip3` target (`target_env = "p3"`). + wstd_p3: { any(feature = "wasip3", target_env = "p3") }, + // True when targeting a wasip2 component, either via the `wasip2` + // feature (default) or the `wasm32-wasip2` target. wasip3 takes + // precedence when both apply. + wstd_p2: { all(any(feature = "wasip2", target_env = "p2"), not(wstd_p3)) }, + } +} diff --git a/src/http/body.rs b/src/http/body.rs index 20e3c66..c093a6e 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -8,25 +8,25 @@ use http::header::CONTENT_LENGTH; use http_body_util::{BodyExt, combinators::UnsyncBoxBody}; use std::fmt; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use crate::http::fields::{header_map_from_wasi, header_map_to_wasi}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use crate::io::AsyncOutputStream; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use std::future::{Future, poll_fn}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use std::pin::{Pin, pin}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use std::task::{Context, Poll}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use crate::runtime::{AsyncPollable, Reactor, WaitFor}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::{ FutureTrailers, IncomingBody as WasiIncomingBody, OutgoingBody as WasiOutgoingBody, }; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::io::streams::{InputStream as WasiInputStream, StreamError}; pub mod util { @@ -70,10 +70,10 @@ enum BodyInner { // a boxed http_body::Body impl Boxed(UnsyncBoxBody), // a body created from a wasi-http incoming-body (p2) - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] Incoming(Incoming), // a body created from a p3 StreamReader - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] P3Stream(P3StreamBody), // a body in memory Complete { @@ -82,7 +82,7 @@ enum BodyInner { }, } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl Body { pub(crate) async fn send(self, outgoing_body: WasiOutgoingBody) -> Result<(), Error> { match self.0 { @@ -141,7 +141,7 @@ impl Body { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl Body { pub(crate) fn from_p3_stream( reader: wit_bindgen::rt::async_support::StreamReader, @@ -161,9 +161,9 @@ impl Body { unreachable!() } match self.0 { - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(), - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] BodyInner::P3Stream(p3) => { // Convert p3 stream body to a boxed body let stream = AsyncInputStream::new(p3.reader.unwrap()); @@ -198,7 +198,7 @@ impl Body { // For p3 streams, read directly using the async read method // instead of going through poll_next (which doesn't properly // persist the read future across polls, causing hangs). - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] if let BodyInner::P3Stream(p3) = prev { let mut stream = AsyncInputStream::new(p3.reader.unwrap()); let mut all_data = Vec::new(); @@ -221,11 +221,11 @@ impl Body { } let boxed_body = match prev { - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] BodyInner::Incoming(i) => i.into_http_body().boxed_unsync(), BodyInner::Boxed(b) => b, BodyInner::Complete { .. } => unreachable!(), - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] BodyInner::P3Stream(_) => unreachable!(), }; let collected = boxed_body.collect().await?; @@ -251,9 +251,9 @@ impl Body { match &self.0 { BodyInner::Boxed(b) => b.size_hint().exact(), BodyInner::Complete { data, .. } => Some(data.len() as u64), - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] BodyInner::Incoming(i) => i.size_hint.content_length(), - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] BodyInner::P3Stream(p3) => p3.size_hint.content_length(), } } @@ -426,28 +426,27 @@ impl fmt::Display for InvalidContentLength { } impl std::error::Error for InvalidContentLength {} -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] struct P3StreamBody { reader: Option>, size_hint: BodyHint, } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl fmt::Debug for P3StreamBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("P3StreamBody").finish() } } - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug)] struct Incoming { body: WasiIncomingBody, size_hint: BodyHint, } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl Incoming { fn into_http_body(self) -> IncomingBody { IncomingBody::new(self.body, self.size_hint) @@ -482,14 +481,14 @@ impl Incoming { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug)] pub struct IncomingBody { state: Option>>, size_hint: BodyHint, } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl IncomingBody { fn new(body: WasiIncomingBody, size_hint: BodyHint) -> Self { Self { @@ -508,7 +507,7 @@ impl IncomingBody { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl HttpBody for IncomingBody { type Data = Bytes; type Error = Error; @@ -564,7 +563,7 @@ impl HttpBody for IncomingBody { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pin_project_lite::pin_project! { #[project = IBSProj] #[derive(Debug)] @@ -583,7 +582,7 @@ pin_project_lite::pin_project! { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug)] struct BodyState { wait: Option>>, @@ -591,10 +590,10 @@ struct BodyState { stream: WasiInputStream, } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] const MAX_FRAME_SIZE: u64 = 64 * 1024; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl BodyState { fn poll_frame( mut self: Pin<&mut Self>, @@ -639,7 +638,7 @@ impl BodyState { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug)] struct TrailersState { wait: Option>>, @@ -647,7 +646,7 @@ struct TrailersState { future_trailers: FutureTrailers, } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl TrailersState { fn new(future_trailers: FutureTrailers) -> Self { Self { diff --git a/src/http/client.rs b/src/http/client.rs index 4d724a7..1c83580 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -20,7 +20,7 @@ impl Client { } /// Send an HTTP request. - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] pub async fn send>(&self, req: Request) -> Result, Error> { use crate::http::request::try_into_outgoing; use crate::http::response::try_from_incoming; @@ -42,7 +42,7 @@ impl Client { } /// Send an HTTP request. - #[cfg(feature = "wasip3")] + #[cfg(wstd_p3)] pub async fn send>(&self, req: Request) -> Result, Error> { use crate::http::request::try_into_wasi_request; use crate::http::response::try_from_wasi_response; @@ -97,7 +97,7 @@ impl Client { } } - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] fn wasi_options_p2( &self, ) -> Result, crate::http::Error> { @@ -108,7 +108,7 @@ impl Client { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub(crate) type P3RequestOptions = RequestOptions; #[derive(Default, Debug, Clone)] @@ -119,7 +119,7 @@ pub(crate) struct RequestOptions { } impl RequestOptions { - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] fn to_wasi_p2(&self) -> Result { let wasi = wasip2::http::types::RequestOptions::new(); if let Some(timeout) = self.connect_timeout { diff --git a/src/http/error.rs b/src/http/error.rs index ab90e2a..3841cbb 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -7,10 +7,10 @@ pub use anyhow::Context; pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; pub use http::method::InvalidMethod; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use wasip2::http::types::{ErrorCode, HeaderError}; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use wasip3::http::types::{ErrorCode, HeaderError}; pub type Error = anyhow::Error; diff --git a/src/http/fields.rs b/src/http/fields.rs index 4ebda6b..7321d83 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -2,13 +2,13 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; use super::{Error, error::Context}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::Fields; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::http::types::Fields; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { let mut output = HeaderMap::new(); for (key, value) in wasi_fields.entries() { @@ -21,7 +21,7 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result Result { let mut output = HeaderMap::new(); for (key, value) in wasi_fields.copy_all() { diff --git a/src/http/method.rs b/src/http/method.rs index 49ffb59..7934182 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,7 +1,7 @@ -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::Method as WasiMethod; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::http::types::Method as WasiMethod; pub use http::Method; diff --git a/src/http/mod.rs b/src/http/mod.rs index ea30a74..0cd5cdc 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -27,11 +27,12 @@ pub mod server; // Conditionally-compiled declarative macro for HTTP server export // // The `#[wstd::http_server]` proc macro delegates to this declarative macro. -// Because `#[macro_export]` macros are compiled in wstd's context, the `cfg` -// checks here use wstd's own feature flags so consumers don't need to define -// `wasip2`/`wasip3` features themselves. +// Because `#[macro_export]` macros are compiled in wstd's context, the +// `wstd_p2` / `wstd_p3` cfg aliases (defined in build.rs) are evaluated against +// wstd's own features and target environment. Consumers don't need to define +// any features themselves. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[macro_export] #[doc(hidden)] macro_rules! __http_server_export { @@ -91,7 +92,7 @@ macro_rules! __http_server_export { }; } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] #[macro_export] #[doc(hidden)] macro_rules! __http_server_export { diff --git a/src/http/request.rs b/src/http/request.rs index 12ffd87..907c6b1 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -9,13 +9,12 @@ use super::{ pub use http::request::{Builder, Request}; - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::outgoing_handler::OutgoingRequest; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::IncomingRequest; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); @@ -56,7 +55,7 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque /// This is used by the `http_server` macro. #[doc(hidden)] -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers()) .context("headers provided by wasi rejected by http::HeaderMap")?; @@ -110,14 +109,13 @@ pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Err request.body(body).context("building request from wasi") } - -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::http::types::{ Request as WasiRequest, RequestOptions as WasiRequestOptions, Scheme as WasiScheme, }; /// Result of converting an http::Request into a p3 WASI Request. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub(crate) struct WasiRequestParts { pub request: WasiRequest, pub body: Body, @@ -126,7 +124,7 @@ pub(crate) struct WasiRequestParts { } /// Convert an http::Request into a p3 WASI Request for sending. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub(crate) fn try_into_wasi_request>( request: Request, request_options: Option<&super::client::P3RequestOptions>, @@ -204,7 +202,7 @@ pub(crate) fn try_into_wasi_request>( /// Convert a p3 WASI Request into an http::Request (for the server handler). #[doc(hidden)] -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub fn try_from_wasi_request( incoming: WasiRequest, completion: wit_bindgen::rt::async_support::FutureReader>, diff --git a/src/http/response.rs b/src/http/response.rs index 11d2f9f..b8fd1de 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -6,11 +6,10 @@ use crate::http::fields::{HeaderMap, header_map_from_wasi}; pub use http::response::{Builder, Response}; - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::IncomingResponse; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; let status = StatusCode::from_u16(incoming.status()) @@ -29,13 +28,12 @@ pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result>, diff --git a/src/http/scheme.rs b/src/http/scheme.rs index fb27b60..1c85f4c 100644 --- a/src/http/scheme.rs +++ b/src/http/scheme.rs @@ -1,7 +1,7 @@ -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::http::types::Scheme as WasiScheme; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::http::types::Scheme as WasiScheme; pub use http::uri::{InvalidUri, Scheme}; diff --git a/src/http/server.rs b/src/http/server.rs index 3180bb0..51cee72 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -18,8 +18,7 @@ //! [`Response`]: crate::http::Response //! [`http_server`]: crate::http_server - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] mod p2 { use crate::http::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; use http::header::CONTENT_LENGTH; @@ -80,17 +79,17 @@ mod p2 { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use p2::*; // In p3, the handler trait is `async fn handle(Request) -> Result`. // The macro generates the appropriate code. No Responder/outparam pattern needed. // p3 server utilities for the macro -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use p3::*; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] mod p3 { use crate::http::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; use http::header::CONTENT_LENGTH; diff --git a/src/io/copy.rs b/src/io/copy.rs index c65fc15..9fa9ab4 100644 --- a/src/io/copy.rs +++ b/src/io/copy.rs @@ -8,7 +8,7 @@ where { // Optimized path when we have an `AsyncInputStream` and an // `AsyncOutputStream` (p2 only — p2 can use wasi splice). - #[cfg(all(feature = "wasip2", not(feature = "wasip3")))] + #[cfg(wstd_p2)] if let Some(reader) = reader.as_async_input_stream() && let Some(writer) = writer.as_async_output_stream() { diff --git a/src/io/mod.rs b/src/io/mod.rs index 90fceee..2e3c9b8 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -9,7 +9,7 @@ mod stdio; mod streams; mod write; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use crate::runtime::AsyncPollable; pub use copy::*; pub use cursor::*; diff --git a/src/io/stdio.rs b/src/io/stdio.rs index 896bea7..a42589d 100644 --- a/src/io/stdio.rs +++ b/src/io/stdio.rs @@ -1,14 +1,14 @@ use super::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Result}; use std::cell::LazyCell; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::cli::terminal_input::TerminalInput; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::cli::terminal_output::TerminalOutput; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::cli::terminal_input::TerminalInput; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::cli::terminal_output::TerminalOutput; /// Use the program's stdin as an `AsyncInputStream`. @@ -19,7 +19,7 @@ pub struct Stdin { } /// Get the program's stdin for use as an `AsyncInputStream`. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub fn stdin() -> Stdin { let stream = AsyncInputStream::new(wasip2::cli::stdin::get_stdin()); Stdin { @@ -29,7 +29,7 @@ pub fn stdin() -> Stdin { } /// Get the program's stdin for use as an `AsyncInputStream`. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub fn stdin() -> Stdin { let (reader, _completion) = wasip3::cli::stdin::read_via_stream(); let stream = AsyncInputStream::new(reader); @@ -76,7 +76,7 @@ pub struct Stdout { } /// Get the program's stdout for use as an `AsyncOutputStream`. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub fn stdout() -> Stdout { let stream = AsyncOutputStream::new(wasip2::cli::stdout::get_stdout()); Stdout { @@ -86,7 +86,7 @@ pub fn stdout() -> Stdout { } /// Get the program's stdout for use as an `AsyncOutputStream`. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub fn stdout() -> Stdout { let (writer, reader) = wasip3::wit_stream::new::(); // Wire the reader end to the WASI stdout sink. The returned future resolves @@ -142,7 +142,7 @@ pub struct Stderr { } /// Get the program's stderr for use as an `AsyncOutputStream`. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub fn stderr() -> Stderr { let stream = AsyncOutputStream::new(wasip2::cli::stderr::get_stderr()); Stderr { @@ -152,7 +152,7 @@ pub fn stderr() -> Stderr { } /// Get the program's stderr for use as an `AsyncOutputStream`. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub fn stderr() -> Stderr { let (writer, reader) = wasip3::wit_stream::new::(); let _completion = wasip3::cli::stderr::write_via_stream(reader); diff --git a/src/io/streams.rs b/src/io/streams.rs index f253a5a..6cf4a98 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -1,4 +1,4 @@ -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] mod p2 { use crate::io::{AsyncPollable, AsyncRead, AsyncWrite}; use crate::runtime::WaitFor; @@ -308,10 +308,10 @@ mod p2 { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use p2::*; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] mod p3 { use crate::io::{AsyncRead, AsyncWrite}; use std::pin::Pin; @@ -554,5 +554,5 @@ mod p3 { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use p3::*; diff --git a/src/lib.rs b/src/lib.rs index f3100d9..866bb08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,13 +74,18 @@ pub use wstd_macro::attr_macro_test as test; // 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. -#[cfg(feature = "wasip3")] +// +// `wstd_p2` and `wstd_p3` are cfg aliases defined in build.rs that dispatch on +// either the explicit feature flag or the target environment. When the +// `wasm32-wasip3` target is used, `target_env = "p3"` is true and consumers +// don't need to set the wasip3 feature manually. +#[cfg(wstd_p3)] #[doc(hidden)] pub mod __internal { pub use wasip3; } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[doc(hidden)] pub mod __internal { pub use wasip2; diff --git a/src/net/mod.rs b/src/net/mod.rs index e4b8b99..bd48389 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -8,7 +8,7 @@ mod tcp_stream; pub use tcp_listener::*; pub use tcp_stream::*; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] fn to_io_err(err: wasip2::sockets::network::ErrorCode) -> io::Error { use wasip2::sockets::network::ErrorCode; match err { @@ -29,7 +29,7 @@ fn to_io_err(err: wasip2::sockets::network::ErrorCode) -> io::Error { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] fn to_io_err(err: wasip3::sockets::types::ErrorCode) -> io::Error { use wasip3::sockets::types::ErrorCode; match err { diff --git a/src/net/tcp_listener.rs b/src/net/tcp_listener.rs index bedff09..1a8c1f1 100644 --- a/src/net/tcp_listener.rs +++ b/src/net/tcp_listener.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use super::{TcpStream, to_io_err}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] mod p2 { use super::*; use crate::runtime::AsyncPollable; @@ -130,10 +130,10 @@ mod p2 { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use p2::*; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] mod p3 { use super::*; use wasip3::sockets::types::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, TcpSocket}; @@ -257,5 +257,5 @@ mod p3 { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use p3::*; diff --git a/src/net/tcp_stream.rs b/src/net/tcp_stream.rs index 3e5f63e..bd2ff45 100644 --- a/src/net/tcp_stream.rs +++ b/src/net/tcp_stream.rs @@ -4,7 +4,7 @@ use std::net::{SocketAddr, ToSocketAddrs}; use super::to_io_err; use crate::io::{self, AsyncInputStream, AsyncOutputStream}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] mod p2 { use super::*; use crate::runtime::AsyncPollable; @@ -189,10 +189,10 @@ mod p2 { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use p2::*; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] mod p3 { use super::*; use wasip3::sockets::types::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, TcpSocket}; @@ -325,5 +325,5 @@ mod p3 { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use p3::*; diff --git a/src/rand/mod.rs b/src/rand/mod.rs index d2d43f1..1b75e5c 100644 --- a/src/rand/mod.rs +++ b/src/rand/mod.rs @@ -1,9 +1,9 @@ //! Random number generation. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::random; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::random; /// Fill the slice with cryptographically secure random bytes. diff --git a/src/runtime/block_on.rs b/src/runtime/block_on.rs index 0f98bbc..b268dff 100644 --- a/src/runtime/block_on.rs +++ b/src/runtime/block_on.rs @@ -1,13 +1,12 @@ use super::{REACTOR, Reactor}; use std::future::Future; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use std::pin::pin; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use std::task::{Context, Poll, Waker}; - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] /// Start the event loop. Blocks until the future completes. pub fn block_on(fut: F) -> F::Output where @@ -73,7 +72,7 @@ where // the runtime is done (native async operations will re-schedule tasks when they // complete). -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] /// Start the event loop. Blocks until the future completes. /// /// Delegates to wit-bindgen's block_on which integrates with the component diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 5ecd385..da8e3e7 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -15,7 +15,7 @@ pub use block_on::block_on; pub use reactor::Reactor; use std::cell::RefCell; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use reactor::{AsyncPollable, WaitFor}; // There are no threads in WASI, so this is just a safe way to thread a single reactor to all diff --git a/src/runtime/reactor.rs b/src/runtime/reactor.rs index 0669a2d..2dcfeb9 100644 --- a/src/runtime/reactor.rs +++ b/src/runtime/reactor.rs @@ -3,7 +3,7 @@ // It maintains a Slab of Pollables, a map of waiters, and a ready list of // runnables. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] mod p2 { use super::super::REACTOR; @@ -467,7 +467,7 @@ mod p2 { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub use p2::*; // @@ -475,7 +475,7 @@ pub use p2::*; // support, so there is no need to manage Pollables. The reactor only manages a // ready list of runnables for cooperative multitasking. -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] mod p3 { use super::super::REACTOR; @@ -564,5 +564,5 @@ mod p3 { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub use p3::*; diff --git a/src/time/duration.rs b/src/time/duration.rs index c63a6a8..de50d1b 100644 --- a/src/time/duration.rs +++ b/src/time/duration.rs @@ -2,10 +2,10 @@ use super::{Instant, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::clocks::monotonic_clock; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::clocks::monotonic_clock; /// A Duration type to represent a span of time, typically used for system diff --git a/src/time/instant.rs b/src/time/instant.rs index be807c9..881d427 100644 --- a/src/time/instant.rs +++ b/src/time/instant.rs @@ -2,10 +2,10 @@ use super::{Duration, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::clocks::monotonic_clock; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::clocks::monotonic_clock; /// A measurement of a monotonically nondecreasing clock. Opaque and useful only @@ -15,11 +15,11 @@ use wasip2::clocks::monotonic_clock; /// without coherence issues, just like if we were implementing this in the /// stdlib. #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pub struct Instant(pub(crate) monotonic_clock::Instant); #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub struct Instant(pub(crate) monotonic_clock::Mark); impl Instant { diff --git a/src/time/mod.rs b/src/time/mod.rs index a2d5017..0d3c041 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -13,38 +13,37 @@ use std::task::{Context, Poll}; use crate::iter::AsyncIterator; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use wasip2::clocks::{ monotonic_clock::{subscribe_duration, subscribe_instant}, wall_clock, }; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use crate::runtime::{AsyncPollable, Reactor}; -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] use pin_project_lite::pin_project; -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] use wasip3::clocks::{monotonic_clock, system_clock}; - /// A measurement of the system clock, useful for talking to external entities /// like the file system or other processes. May be converted losslessly to a /// more useful `std::time::SystemTime` to provide more methods. -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug, Clone, Copy)] #[allow(dead_code)] pub struct SystemTime(wall_clock::Datetime); -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl SystemTime { pub fn now() -> Self { Self(wall_clock::now()) } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl From for std::time::SystemTime { fn from(st: SystemTime) -> Self { std::time::SystemTime::UNIX_EPOCH @@ -53,19 +52,19 @@ impl From for std::time::SystemTime { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] #[derive(Debug, Clone, Copy)] #[allow(dead_code)] pub struct SystemTime(system_clock::Instant); -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl SystemTime { pub fn now() -> Self { Self(system_clock::now()) } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl From for std::time::SystemTime { fn from(st: SystemTime) -> Self { // p3 system_clock::Instant has i64 seconds @@ -81,7 +80,6 @@ impl From for std::time::SystemTime { } } - /// An async iterator representing notifications at fixed interval. pub fn interval(duration: Duration) -> Interval { Interval { duration } @@ -102,12 +100,11 @@ impl AsyncIterator for Interval { } } - -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] #[derive(Debug)] pub struct Timer(Option); -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl Timer { pub fn never() -> Timer { Timer(None) @@ -129,7 +126,7 @@ impl Timer { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] pin_project! { /// Future created by [`Timer::wait`] #[must_use = "futures do nothing unless polled or .awaited"] @@ -139,7 +136,7 @@ pin_project! { } } -#[cfg(all(feature = "wasip2", not(feature = "wasip3")))] +#[cfg(wstd_p2)] impl Future for Wait { type Output = Instant; @@ -155,27 +152,26 @@ impl Future for Wait { } } - -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] pub struct Timer { kind: TimerKind, } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] enum TimerKind { Never, After(Duration), At(Instant), } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl std::fmt::Debug for Timer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Timer").finish() } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl Timer { pub fn never() -> Timer { Timer { @@ -205,13 +201,13 @@ impl Timer { } } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] #[must_use = "futures do nothing unless polled or .awaited"] pub struct Wait { inner: Pin>>, } -#[cfg(feature = "wasip3")] +#[cfg(wstd_p3)] impl Future for Wait { type Output = Instant; @@ -223,7 +219,6 @@ impl Future for Wait { } } - #[cfg(test)] mod test { use super::*;