Skip to content

[do not merge] add p3 support#129

Draft
yoshuawuyts wants to merge 18 commits into
mainfrom
p3
Draft

[do not merge] add p3 support#129
yoshuawuyts wants to merge 18 commits into
mainfrom
p3

Conversation

@yoshuawuyts

Copy link
Copy Markdown
Member

This is a follow-up to #128 and #127, showing what a p3 implementation on top of a sys structure would look like.

Just like when filing #128, I've had the computer generate this and haven't yet reviewed this in-depth. The relevant commit for p3 support is 9a9fa0d. I'm filing this PR mostly to show how p3 support might play out on top of the sys/ structure so we can make an informed decision. If we think this structure seems right, I'll close this PR and once we merge something like #127 I can go and bring in p3 support one module at a time.

Thanks!

yoshuawuyts and others added 18 commits July 25, 2026 14:19
This is a pure `git mv` relocation with no content changes, so git
records clean renames and `git blame`/`--follow` lineage is preserved.
The build is intentionally red after this commit; backend wiring and
crate-root re-export shims are added in the following commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restores a green build after the pure relocation. The crate root now
selects a platform backend via a single cfg-if in src/sys/mod.rs, and
each moved module is re-exported through a thin shim so the public API
is preserved exactly:

* add `mod sys` plus src/sys/mod.rs (cfg-if) and src/sys/p2/mod.rs
* add crate-root shims: http.rs, net.rs, rand.rs, runtime.rs, time.rs
* re-export the moved stdio/streams via src/sys/p2/io and io/mod.rs
* repoint the few internal `super::`/private-module paths the move broke
* add the cfg-if dependency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Spell out, in `src/sys/mod.rs`, the duck-typed contract each `src/sys`
backend must satisfy so the crate-root modules can be target-agnostic
facades. Add a private `const _` block that statically checks the parts
the facades depend on (the IO stream traits today), so backend drift
fails fast with a clear message instead of surfacing deep inside a
facade.

No behavior or public-API change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Promote the portable time types — `Duration`, `Instant`, `Interval`,
`interval`, `Timer`, `Wait`, and the crate-internal `utils::timeout_err`
— out of the wasip2 backend and into the `crate::time` facade, where
they are written once with no `#[cfg]` and own all of their own
arithmetic.

The wasip2 backend (`src/sys/p2/time`) keeps only the primitives that
genuinely depend on the WASI 0.2 clocks: the `MonotonicInstant` /
`MonotonicDuration` nanosecond aliases, `now`, `SystemTime`, and a
`Sleep` future built by `sleep_until`. `Timer` now records its deadline
at construction (`TimerKind::{Never, At}`) and builds a fresh backend
`Sleep` on each `wait`, so a second backend (p3) only has to supply a
different `Sleep`/`sleep_until` rather than re-implementing the facade.

Extend the backend-contract assertions in `src/sys/mod.rs` accordingly.
Public API is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The length guard and slice copy in `get_random_bytes` /
`get_insecure_random_bytes` are identical across backends; only the host
RNG call differs. Move the two public functions up into the `crate::rand`
facade and reduce the wasip2 backend to `random_bytes` /
`insecure_random_bytes` primitives that return the raw bytes.

Public API is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`spawn` and the `async_task::Task` re-export are identical across
backends; only `block_on` and the `Reactor` internals are platform
specific. Move `spawn` and `Task` up into the `crate::runtime` facade
and re-export `block_on`/`Reactor`/`AsyncPollable`/`WaitFor` from the
wasip2 backend. `REACTOR` and `Reactor` stay in the backend.

Public API is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert `src/http.rs` from a blanket `pub use crate::sys::http::*` shim into a
real facade: it owns the portable surface (`StatusCode`, `Uri`/`Authority`/
`PathAndQuery`, the `error` module of `anyhow`/`http`-crate error types, and the
`body`/`request`/`response`/`server` public submodule paths) and re-exports the
backend's concrete types (`Client`, `Method`, `Scheme`, `HeaderMap`, `Body`,
...) through `crate::sys::http::*`. This keeps every public path identical while
moving the target-agnostic shape out of the backend, so a future p3 backend only
has to supply the platform pieces.

The backend `sys::p2::http` now exposes just the platform impl: submodules with
`pub`/`pub(crate)` visibility plus `pub use wasip2::http::types::{ErrorCode,
HeaderError}`. Its impl files import the public error/value types from the facade
(`crate::http::*`) instead of the removed backend-level re-exports, and the
public `server` module doc moves up to the facade.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`src/net.rs` was a blanket `pub use crate::sys::net::*`. Make it a real
facade: declare the crate's public network surface -- `TcpStream`,
`TcpListener`, `ReadHalf`, `WriteHalf`, `Incoming` -- as thin wrappers at the
crate root that delegate every operation to the selected backend's
implementation under `crate::sys::net`.

The one genuinely portable piece, `TcpStream::connect`'s `ToSocketAddrs`
resolve-and-retry loop, is written once here against the backend's
`connect_addr` primitive (removed from the backend), so a second backend
inherits it for free. Socket-shutdown cleanup still runs through the inner
backend handles' own `Drop`; the facade keeps explicit `Drop` impls so the
public drop contract is unchanged.

Public API is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that `wstd` and `wstd-axum` only compile for `wasm32-wasip2` (the `sys`
backend is cfg-gated and other targets hit `compile_error!`), the CI steps that
built them for the host target fail to resolve `crate::sys::*`. Scope those
steps to the target:

- `check` and `Clippy` build `wstd`/`wstd-axum` with `--target wasm32-wasip2
  --all-targets` and keep the `test-programs` host harness on the host.
- `Docs` builds with `--target wasm32-wasip2`.
- `ci/publish.rs` passes `--target wasm32-wasip2` when packaging `wstd` and
  `wstd-axum`.

`--all-targets` newly lints the `#[cfg(test)]` code, so regroup the digit
literals in the `time` duration tests to satisfy `inconsistent_digit_grouping`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the test failures introduced by the p2/p3 refactor:

- wstd-axum: add a `json` feature (enabled by default) that turns on
  `wstd/json`. The switch to `default-features = false` had dropped json,
  breaking the weather example's `Body::json()`.
- p3 time: make `Wait` hold a `Send` future so streaming response bodies
  (which await `task::sleep`) satisfy `Body::from_try_stream`'s `Send`
  bound. Fixes the `http_server` example under wasip3.
- p3 net: give `TcpStream`/`TcpListener` interior mutability so they expose
  the same shared-reference API as p2 (`io::copy(&stream, &stream)`,
  `incoming(&self)`), add `TcpStream::peer_addr`, and replace the unsound
  raw-pointer `split` with safe shared references. Fixes the
  `tcp_echo_server` example under wasip3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Under WASIp3 a synchronous `wasi:cli/run` task cannot block on
async-lowered imports: `block_on` (driven by `waitable-set.wait`) traps
with "cannot block a synchronous task before returning". So `block_on`
stays a p2-only concept.

For p3, `#[wstd::main]` now async-lifts the export the same way
`#[wstd::http_server]` does: it implements the async `wasi:cli/run`
guest directly and exports it via `wasip3::cli::command::export!`, so
the body can `.await` async-lowered imports (timers, sockets, http)
without trapping. The bin's `fn main` remains as a dead entry point.

The proc macro now delegates to a cfg-split `__main_export!` declarative
macro in wstd (p2 keeps block_on; p3 async-lifts), mirroring
`__http_server_export!`, so the `wstd_p2`/`wstd_p3` cfg aliases are
evaluated in wstd's context for downstream consumers. A `__MainReturn`
helper maps `()` / `Result<(), E: Debug>` to the `Result<(), ()>` the
export requires, printing errors to stderr on failure.

Also apply a pending rustfmt fix to src/sys/p3/io.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The p3 `Reactor::spawn` had two bugs that left `wstd::runtime::spawn`
non-functional on the p3 backend:

1. Runnables were pushed onto an in-`Reactor` `ready_list` that nothing
   ever drained. p3 `block_on` delegates to wit-bindgen's
   component-model async executor and never looked at `ready_list`, so
   spawned futures were never polled.

2. `Reactor::current()` read a thread-local `REACTOR` that is only set
   by `block_on`. But `#[wstd::main]` on p3 async-lifts `wasi:cli/run`
   directly and never calls `block_on`, so calling `spawn` from `main`
   (e.g. the `tcp_echo_server` example) hit the `.expect(..)` and
   aborted.

Fix both by driving each `async_task` runnable on wit-bindgen's executor
via `async_support::spawn`, and making p3 `Reactor::current()`
infallible since that executor is live in any async component task. The
`Reactor` becomes a stateless marker; `REACTOR` now only guards against
nested `block_on`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
wasmtime 45 renamed the builtin to `component-model-more-async-builtins`
(the 🚝 feature); the old `-Wcomponent-model-async-builtins` name is
rejected with a non-zero exit, so `cargo run/test --target wasm32-wasip3`
could never start.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `wstd tests (wasip3)` CI step was broken: p3's `block_on` blocks on
`waitable-set.wait`, which traps ("cannot block a synchronous task before
returning") when driven from libtest's synchronously-lifted `wasi:cli/run`
`main`. p2's `block_on` blocks via `wasi:io/poll.poll`, which a sync task may
do, so only p3 was affected.

Add a dependency-free `wstd::test_main!` macro (plus a `#[doc(hidden)]`
`__test` support module) that provides a `harness = false` entry point for
integration tests:

* on p3 it async-lifts the test binary's `wasi:cli/run` export, so per-test
  `block_on` calls are made from an async task and no longer trap;
* on p2 it emits a plain `fn main` that runs each test via `block_on`.

Both variants print libtest-style status lines and exit non-zero when a test
fails (whether it panics or returns `Err`), so `cargo test` still reports
failures. Convert all integration tests to plain `async fn`s driven by
`test_main!` and mark each `[[test]]` target `harness = false`.

To let a single `wasm32-wasip2` build serve both backends, add the
component-model async runner flags (`-Wcomponent-model-async`,
`-Wcomponent-model-more-async-builtins`, `-Wcomponent-model-async-stackful`,
`-Sp3`) to the `wasm32-wasip2` runner. These are inert for plain p2 components,
so the wasip2 suite is unaffected.

Finally, gate the two blocking `src/time.rs` unit tests (`timer_now`,
`timer_after_100_milliseconds`) with `#[cfg_attr(wstd_p3, ignore)]`: they run
under libtest's default (synchronous) harness in the `[lib]` target, which
still traps on p3. Their behavior is covered by the integration test suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant