Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 31 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
66 changes: 66 additions & 0 deletions src/wasip2.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Output> {
// `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()
}
}