diff --git a/Cargo.toml b/Cargo.toml index 6db1876..b2e2627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ serde_json = { workspace = true, optional = true } wasip2.workspace = true [target.'cfg(all(target_os = "wasi", target_env = "p3"))'.dependencies] -wasip3.workspace = true +wasip3 = { workspace = true, features = ["async-spawn"] } [dev-dependencies] anyhow.workspace = true diff --git a/src/lib.rs b/src/lib.rs index ad5aad4..45c6a85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,21 +67,10 @@ pub mod iter; pub mod net; #[cfg(target_os = "wasi")] pub mod rand; -#[cfg(all(target_os = "wasi", target_env = "p2"))] pub mod runtime; -#[cfg(all(target_os = "wasi", target_env = "p3"))] -pub mod runtime { - pub fn block_on(fut: F) -> F::Output - where - F: Future, - T: 'static, - { - wasip3::wit_bindgen::block_on(fut) - } -} -#[cfg(all(target_os = "wasi", target_env = "p2"))] +#[cfg(target_os = "wasi")] pub mod task; -#[cfg(all(target_os = "wasi", target_env = "p2"))] +#[cfg(target_os = "wasi")] pub mod time; #[cfg(all(target_os = "wasi", target_env = "p2"))] diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 24b9fc2..a92d7fa 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,25 +1,36 @@ //! Async event loop support. //! -//! 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 +//! On WASI 0.2 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 used 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. +//! +//! On WASI 0.3 [`block_on`] can be used to drive a future, but an async +//! function can also be directly exported and will be driven by the host. #![deny(missing_debug_implementations, nonstandard_style)] #![warn(missing_docs, unreachable_pub)] +pub use ::async_task::Task; + +#[cfg(target_env = "p2")] mod block_on; +#[cfg(target_env = "p2")] mod reactor; -pub use ::async_task::Task; +#[cfg(target_env = "p2")] pub use block_on::block_on; +#[cfg(target_env = "p2")] pub use reactor::{AsyncPollable, Reactor, WaitFor}; +#[cfg(target_env = "p2")] 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 // use sites in the background. +#[cfg(target_env = "p2")] std::thread_local! { pub(crate) static REACTOR: RefCell> = const { RefCell::new(None) }; } @@ -27,6 +38,7 @@ pub(crate) static REACTOR: RefCell> = const { RefCell::new(None) /// Spawn a `Future` as a `Task` on the current `Reactor`. /// /// Panics if called from outside `block_on`. +#[cfg(target_env = "p2")] pub fn spawn(fut: F) -> Task where F: std::future::Future + 'static, @@ -34,3 +46,26 @@ where { Reactor::current().spawn(fut) } + +#[cfg(target_env = "p3")] +pub use ::async_task::Runnable; +#[cfg(target_env = "p3")] +pub use wasip3::wit_bindgen::block_on; + +/// Spawn a `Future` as a `Task` on the WASI 0.3 async runtime. +#[cfg(target_env = "p3")] +pub fn spawn(fut: F) -> Task +where + F: std::future::Future + 'static, + T: 'static, +{ + let (runnable, task) = async_task::spawn_local(fut, |runnable: Runnable| { + // Scheduling the task is accomplished by spawning a future which + // executes the `run` method. + wasip3::spawn_local(async move { + let _ = runnable.run(); + }); + }); + runnable.schedule(); + task +} diff --git a/src/time/duration.rs b/src/time/duration.rs index 7f67ceb..15a6103 100644 --- a/src/time/duration.rs +++ b/src/time/duration.rs @@ -1,7 +1,10 @@ use super::{Instant, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; +#[cfg(target_env = "p2")] use wasip2::clocks::monotonic_clock; +#[cfg(target_env = "p3")] +use wasip3::clocks::monotonic_clock; /// A Duration type to represent a span of time, typically used for system /// timeouts. diff --git a/src/time/instant.rs b/src/time/instant.rs index 6e9cf97..03b7357 100644 --- a/src/time/instant.rs +++ b/src/time/instant.rs @@ -1,7 +1,10 @@ use super::{Duration, Wait}; use std::future::IntoFuture; use std::ops::{Add, AddAssign, Sub, SubAssign}; -use wasip2::clocks::monotonic_clock; +#[cfg(target_env = "p2")] +use wasip2::clocks::monotonic_clock::{self, Instant as WasiInstant}; +#[cfg(target_env = "p3")] +use wasip3::clocks::monotonic_clock::{self, Mark as WasiInstant}; /// A measurement of a monotonically nondecreasing clock. Opaque and useful only /// with Duration. @@ -10,7 +13,7 @@ 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)] -pub struct Instant(pub(crate) monotonic_clock::Instant); +pub struct Instant(pub(crate) WasiInstant); impl Instant { /// Returns an instant corresponding to "now". @@ -24,7 +27,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..b715c11 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -1,5 +1,6 @@ //! Async time interfaces. +#[cfg(target_env = "p2")] pub(crate) mod utils; mod duration; @@ -7,26 +8,24 @@ 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 wasip2::clocks::{ - monotonic_clock::{subscribe_duration, subscribe_instant}, - wall_clock, -}; +#[cfg(target_env = "p2")] +use wasip2::clocks::wall_clock::{self, Datetime}; +#[cfg(target_env = "p3")] +use wasip3::clocks::system_clock::{self as wall_clock, Instant as Datetime}; -use crate::{ - iter::AsyncIterator, - runtime::{AsyncPollable, Reactor}, -}; +use crate::iter::AsyncIterator; +#[cfg(target_env = "p2")] +use crate::runtime::{AsyncPollable, Reactor}; /// 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. #[derive(Debug, Clone, Copy)] #[allow(dead_code)] -pub struct SystemTime(wall_clock::Datetime); +pub struct SystemTime(Datetime); impl SystemTime { pub fn now() -> Self { @@ -35,11 +34,23 @@ impl SystemTime { } impl From for std::time::SystemTime { + #[cfg(target_env = "p2")] fn from(st: SystemTime) -> Self { std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(st.0.seconds) + std::time::Duration::from_nanos(st.0.nanoseconds.into()) } + + #[cfg(target_env = "p3")] + fn from(st: SystemTime) -> Self { + let mut result = std::time::SystemTime::UNIX_EPOCH; + if st.0.seconds < 0 { + result -= std::time::Duration::from_secs(st.0.seconds.unsigned_abs()); + } else { + result += std::time::Duration::from_secs(st.0.seconds.unsigned_abs()); + } + result + std::time::Duration::from_nanos(st.0.nanoseconds.into()) + } } /// An async iterator representing notifications at fixed interval. @@ -62,58 +73,191 @@ impl AsyncIterator for Interval { } } -#[derive(Debug)] -pub struct Timer(Option); +#[cfg(target_env = "p2")] +mod timer { + use super::*; + use pin_project_lite::pin_project; + use wasip2::clocks::monotonic_clock::{subscribe_duration, subscribe_instant}; + + #[derive(Debug)] + pub struct Timer(Option); -impl Timer { - pub fn never() -> Timer { - Timer(None) + impl Timer { + pub fn never() -> Timer { + Timer(None) + } + pub fn at(deadline: Instant) -> Timer { + let pollable = Reactor::current().schedule(subscribe_instant(deadline.0)); + Timer(Some(pollable)) + } + pub fn after(duration: Duration) -> Timer { + let pollable = Reactor::current().schedule(subscribe_duration(duration.0)); + Timer(Some(pollable)) + } + pub fn set_after(&mut self, duration: Duration) { + *self = Self::after(duration); + } + pub fn wait(&self) -> Wait { + let wait_for = self.0.as_ref().map(AsyncPollable::wait_for); + Wait { wait_for } + } } - pub fn at(deadline: Instant) -> Timer { - let pollable = Reactor::current().schedule(subscribe_instant(deadline.0)); - Timer(Some(pollable)) + + pin_project! { + /// Future created by [`Timer::wait`] + #[must_use = "futures do nothing unless polled or .awaited"] + pub struct Wait { + #[pin] + wait_for: Option + } } - pub fn after(duration: Duration) -> Timer { - let pollable = Reactor::current().schedule(subscribe_duration(duration.0)); - Timer(Some(pollable)) + + impl Future for Wait { + type Output = Instant; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.wait_for.as_pin_mut() { + None => Poll::Pending, + Some(f) => match f.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(()) => Poll::Ready(Instant::now()), + }, + } + } } - pub fn set_after(&mut self, duration: Duration) { - *self = Self::after(duration); +} + +#[cfg(target_env = "p3")] +mod timer { + use super::*; + use wasip3::clocks::monotonic_clock::{wait_for, wait_until}; + + #[derive(Debug)] + pub struct Timer(TimerInner); + + enum TimerInner { + Never, + At(Instant), + After(Duration), } - pub fn wait(&self) -> Wait { - let wait_for = self.0.as_ref().map(AsyncPollable::wait_for); - Wait { wait_for } + + impl std::fmt::Debug for TimerInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Timer") + } } -} -pin_project! { - /// Future created by [`Timer::wait`] - #[must_use = "futures do nothing unless polled or .awaited"] - pub struct Wait { - #[pin] - wait_for: Option + impl Timer { + pub fn never() -> Timer { + Timer(TimerInner::Never) + } + pub fn at(deadline: Instant) -> Timer { + Timer(TimerInner::At(deadline)) + } + pub fn after(duration: Duration) -> Timer { + Timer(TimerInner::After(duration)) + } + pub fn set_after(&mut self, duration: Duration) { + *self = Self::after(duration); + } + pub fn wait(&self) -> Wait { + match &self.0 { + TimerInner::Never => Wait(Box::pin(std::future::pending())), + TimerInner::At(instant) => Wait(Box::pin(wait_until(instant.0))), + TimerInner::After(duration) => Wait(Box::pin(wait_for(duration.0))), + } + } } -} -impl Future for Wait { - type Output = Instant; + /// Future created by [`Timer::wait`]. + #[must_use = "futures do nothing unless polled or .awaited"] + pub struct Wait(Pin>>); + + impl Future for Wait { + type Output = Instant; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - match this.wait_for.as_pin_mut() { - None => Poll::Pending, - Some(f) => match f.poll(cx) { + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.0.as_mut().poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(()) => Poll::Ready(Instant::now()), - }, + } } } } +pub use timer::*; + #[cfg(test)] mod test { use super::*; + #[cfg(target_env = "p2")] + fn system_time(seconds: u64, nanoseconds: u32) -> SystemTime { + SystemTime(Datetime { + seconds, + nanoseconds, + }) + } + + #[cfg(target_env = "p3")] + fn system_time(seconds: i64, nanoseconds: u32) -> SystemTime { + SystemTime(Datetime { + seconds, + nanoseconds, + }) + } + + #[test] + fn system_time_conversion_at_epoch() { + let actual: std::time::SystemTime = system_time(0, 0).into(); + + assert_eq!(actual, std::time::SystemTime::UNIX_EPOCH); + } + + #[test] + fn system_time_conversion_after_epoch() { + let actual: std::time::SystemTime = system_time(1, 999_999_999).into(); + let elapsed = actual + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap(); + + assert_eq!(elapsed, std::time::Duration::new(1, 999_999_999)); + } + + #[cfg(target_env = "p3")] + #[test] + fn system_time_conversion_just_before_epoch() { + let actual: std::time::SystemTime = system_time(-1, 999_999_999).into(); + let before_epoch = std::time::SystemTime::UNIX_EPOCH + .duration_since(actual) + .unwrap(); + + assert_eq!(before_epoch, std::time::Duration::from_nanos(1)); + } + + #[cfg(target_env = "p3")] + #[test] + fn system_time_conversion_at_seconds_limits() { + for seconds in [i64::MIN, i64::MAX] { + let actual: std::time::SystemTime = system_time(seconds, 0).into(); + let elapsed = if seconds < 0 { + std::time::SystemTime::UNIX_EPOCH + .duration_since(actual) + .unwrap() + } else { + actual + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + }; + + assert_eq!( + elapsed, + std::time::Duration::from_secs(seconds.unsigned_abs()) + ); + } + } + async fn debug_duration(what: &str, f: impl Future) { let start = Instant::now(); let now = f.await; diff --git a/tests/runtime_spawn.rs b/tests/runtime_spawn.rs new file mode 100644 index 0000000..29e9e96 --- /dev/null +++ b/tests/runtime_spawn.rs @@ -0,0 +1,76 @@ +use std::cell::Cell; +use std::future::pending; +use std::rc::Rc; + +use futures_lite::future::yield_now; + +struct DropFlag(Rc>); + +impl Drop for DropFlag { + fn drop(&mut self) { + self.0.set(true); + } +} + +async fn wait_until(flag: &Cell) { + while !flag.get() { + yield_now().await; + } +} + +#[wstd::test] +async fn spawn_detach_cancel_and_drop() { + assert_eq!(wstd::runtime::spawn(async { 42 }).await, 42); + + // Check that a detached task completes eventually. + let detached_completed = Rc::new(Cell::new(false)); + let completed = detached_completed.clone(); + wstd::runtime::spawn(async move { + yield_now().await; + completed.set(true); + }) + .detach(); + + assert!(!detached_completed.get()); + wait_until(&detached_completed).await; + assert!(detached_completed.get()); + + // Check that calling `cancel` on a task cancels and drops the spawned + // future. + let canceled_started = Rc::new(Cell::new(false)); + let canceled_dropped = Rc::new(Cell::new(false)); + let canceled_completed = Rc::new(Cell::new(false)); + let started = canceled_started.clone(); + let dropped = canceled_dropped.clone(); + let completed = canceled_completed.clone(); + let task = wstd::runtime::spawn(async move { + let _drop_flag = DropFlag(dropped); + started.set(true); + pending::<()>().await; + completed.set(true); + }); + + wait_until(&canceled_started).await; + assert_eq!(task.cancel().await, None); + assert!(canceled_dropped.get()); + assert!(!canceled_completed.get()); + + // Check that dropping a task cancels and drops the spawned future. + let dropped_started = Rc::new(Cell::new(false)); + let dropped_dropped = Rc::new(Cell::new(false)); + let dropped_completed = Rc::new(Cell::new(false)); + let started = dropped_started.clone(); + let dropped = dropped_dropped.clone(); + let completed = dropped_completed.clone(); + let task = wstd::runtime::spawn(async move { + let _drop_flag = DropFlag(dropped); + started.set(true); + pending::<()>().await; + completed.set(true); + }); + + wait_until(&dropped_started).await; + drop(task); + wait_until(&dropped_dropped).await; + assert!(!dropped_completed.get()); +} diff --git a/tests/sleep.rs b/tests/sleep.rs index c888302..446736f 100644 --- a/tests/sleep.rs +++ b/tests/sleep.rs @@ -1,6 +1,6 @@ -#![cfg(all(target_os = "wasi", target_env = "p2"))] - +use std::cell::RefCell; use std::error::Error; +use std::rc::Rc; use wstd::task::sleep; use wstd::time::Duration; @@ -9,3 +9,23 @@ async fn just_sleep() -> Result<(), Box> { sleep(Duration::from_secs(1)).await; Ok(()) } + +#[wstd::test] +async fn concurrent_sleeps_wake_in_deadline_order() { + let wake_order = Rc::new(RefCell::new(Vec::new())); + let mut tasks = Vec::new(); + + for (delay, task) in [(60, "slow"), (20, "fast"), (40, "medium")] { + let wake_order = wake_order.clone(); + tasks.push(wstd::runtime::spawn(async move { + sleep(Duration::from_millis(delay)).await; + wake_order.borrow_mut().push(task); + })); + } + + for task in tasks { + task.await; + } + + assert_eq!(*wake_order.borrow(), ["fast", "medium", "slow"]); +}