diff --git a/Cargo.toml b/Cargo.toml index 3b62542..7508614 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,13 @@ Timeouts for futures. gloo-timers = { version = "0.4.0", features = ["futures"], optional = true } send_wrapper = { version = "0.6.0", optional = true } +# WASI Preview 2 backend: a `wasi:clocks` timer driven by the `wstd` reactor. +# Pulled in only for `wasm32-wasip2`, where the default thread-based backend +# cannot run (the component model is single-threaded). +[target.'cfg(all(target_arch = "wasm32", target_os = "wasi", target_env = "p2"))'.dependencies] +wstd = "0.6" +wasip2 = "1.0" + [dev-dependencies] async-std = { version = "1.13", features = ["attributes"] } futures = "0.3.1" diff --git a/src/lib.rs b/src/lib.rs index 865f5d3..62d660a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,12 +16,37 @@ #![deny(missing_docs)] #![warn(missing_debug_implementations)] -#[cfg(not(all(target_arch = "wasm32", feature = "wasm-bindgen")))] -mod native; -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +// On `wasm32-wasip2` the thread-based `native` backend cannot run (the WASI +// component model is single-threaded), so use a `wasi:clocks`-backed timer. +// `target_env = "p2"` selects WASI Preview 2 specifically, leaving `wasip1` +// (and any future preview) on the `native` backend. +#[cfg(all(target_arch = "wasm32", target_os = "wasi", target_env = "p2"))] +mod wasip2; +#[cfg(all(target_arch = "wasm32", target_os = "wasi", target_env = "p2"))] +pub use self::wasip2::Delay; + +// Browser wasm with the `wasm-bindgen` feature: `setTimeout` via gloo-timers. +#[cfg(all( + target_arch = "wasm32", + feature = "wasm-bindgen", + not(all(target_os = "wasi", target_env = "p2")) +))] mod wasm; +#[cfg(all( + target_arch = "wasm32", + feature = "wasm-bindgen", + not(all(target_os = "wasi", target_env = "p2")) +))] +pub use self::wasm::Delay; -#[cfg(not(all(target_arch = "wasm32", feature = "wasm-bindgen")))] +// All other targets: the thread-backed timer wheel. +#[cfg(not(any( + all(target_arch = "wasm32", target_os = "wasi", target_env = "p2"), + all(target_arch = "wasm32", feature = "wasm-bindgen") +)))] +mod native; +#[cfg(not(any( + all(target_arch = "wasm32", target_os = "wasi", target_env = "p2"), + all(target_arch = "wasm32", feature = "wasm-bindgen") +)))] pub use self::native::Delay; -#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] -pub use self::wasm::Delay; diff --git a/src/wasip2.rs b/src/wasip2.rs new file mode 100644 index 0000000..99706cc --- /dev/null +++ b/src/wasip2.rs @@ -0,0 +1,66 @@ +//! A version of `Delay` that works on `wasm32-wasip2`. +//! +//! The default `native` backend relies on a background timer thread, which the +//! WASI Preview 2 component model cannot spawn (it is single-threaded). This +//! backend instead arms a `wasi:clocks/monotonic-clock` timer and awaits the +//! resulting pollable through the [`wstd`] reactor, so it integrates with any +//! executor built on `wstd::runtime` (e.g. `#[wstd::main]` / `wstd::runtime::block_on`). + +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use wstd::runtime::{AsyncPollable, WaitFor}; + +/// A future which will fire at `dur` time into the future, backed by +/// `wasi:clocks/monotonic-clock` and driven by the `wstd` reactor. +pub struct Delay { + wait_for: WaitFor, +} + +// SAFETY: `wasm32-wasip2` is single-threaded; the underlying WASI pollable +// handle is a plain integer that is trivially safe to move across the +// non-existent thread boundary. `Delay` must be `Send + Sync` to match the +// other backends' API. +unsafe impl Send for Delay {} +unsafe impl Sync for Delay {} + +impl Delay { + /// Creates a new future which will fire at `dur` time into the future. + /// + /// Must be polled from within a `wstd::runtime` executor context. + pub fn new(dur: Duration) -> Delay { + Delay { + wait_for: arm(dur), + } + } + + /// Resets the timeout to fire `dur` time into the future. + pub fn reset(&mut self, dur: Duration) { + self.wait_for = arm(dur); + } +} + +/// Arm a monotonic-clock timer for `dur` and wrap it as an awaitable pollable. +fn arm(dur: Duration) -> WaitFor { + let ns = dur.as_nanos().min(u64::MAX as u128) as u64; + let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(ns); + AsyncPollable::new(pollable).wait_for() +} + +impl Future for Delay { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // `WaitFor` is `Unpin`, so projecting through `get_mut` is sound. + Pin::new(&mut self.get_mut().wait_for).poll(cx) + } +} + +impl fmt::Debug for Delay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Delay").finish() + } +}