From 6f5390c011935ca45c3767636b2d868fcf068dec Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Tue, 2 Sep 2025 15:43:09 -0700
Subject: [PATCH 1/6] chore: drop dep on futures-core, ready! macro is in std
---
Cargo.toml | 1 -
src/future/delay.rs | 3 +--
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 9edfd87..8dc1637 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,6 @@ default = ["json"]
json = ["dep:serde", "dep:serde_json"]
[dependencies]
-futures-core.workspace = true
http.workspace = true
itoa.workspace = true
pin-project-lite.workspace = true
diff --git a/src/future/delay.rs b/src/future/delay.rs
index 48d3b70..d8fcdbf 100644
--- a/src/future/delay.rs
+++ b/src/future/delay.rs
@@ -1,7 +1,6 @@
-use futures_core::ready;
use std::future::Future;
use std::pin::Pin;
-use std::task::{Context, Poll};
+use std::task::{ready, Context, Poll};
use pin_project_lite::pin_project;
From 08cb00f810e7d84e51cacfe833a9ef567799e376 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Tue, 2 Sep 2025 16:42:56 -0700
Subject: [PATCH 2/6] feat: event loop is now based on async-task
---
Cargo.toml | 2 +
src/lib.rs | 14 +++++-
src/runtime/block_on.rs | 100 +++++++++++++++++-----------------------
src/runtime/reactor.rs | 89 ++++++++++++++++++++++++-----------
4 files changed, 118 insertions(+), 87 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 8dc1637..b4538ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ default = ["json"]
json = ["dep:serde", "dep:serde_json"]
[dependencies]
+async-task.workspace = true
http.workspace = true
itoa.workspace = true
pin-project-lite.workspace = true
@@ -62,6 +63,7 @@ authors = [
[workspace.dependencies]
anyhow = "1"
+async-task = "4.7"
cargo_metadata = "0.22"
clap = { version = "4.5.26", features = ["derive"] }
futures-core = "0.3.19"
diff --git a/src/lib.rs b/src/lib.rs
index bb34042..9fdf435 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,5 @@
#![allow(async_fn_in_trait)]
#![warn(future_incompatible, unreachable_pub)]
-#![forbid(unsafe_code)]
//#![deny(missing_debug_implementations)]
//#![warn(missing_docs)]
//#![forbid(rustdoc::missing_doc_code_examples)]
@@ -55,15 +54,26 @@
//! These are unique capabilities provided by WASI 0.2, and because this library
//! is specific to that are exposed from here.
+// We need unsafe code in the runtime.
+pub mod runtime;
+
+// All other mods do not require unsafe.
+#[forbid(unsafe_code)]
pub mod future;
+#[forbid(unsafe_code)]
#[macro_use]
pub mod http;
+#[forbid(unsafe_code)]
pub mod io;
+#[forbid(unsafe_code)]
pub mod iter;
+#[forbid(unsafe_code)]
pub mod net;
+#[forbid(unsafe_code)]
pub mod rand;
-pub mod runtime;
+#[forbid(unsafe_code)]
pub mod task;
+#[forbid(unsafe_code)]
pub mod time;
pub use wstd_macro::attr_macro_http_server as http_server;
diff --git a/src/runtime/block_on.rs b/src/runtime/block_on.rs
index d38a9de..77645a9 100644
--- a/src/runtime/block_on.rs
+++ b/src/runtime/block_on.rs
@@ -2,14 +2,13 @@ use super::{Reactor, REACTOR};
use std::future::Future;
use std::pin::pin;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::Arc;
-use std::task::{Context, Poll, Wake, Waker};
+use std::task::{Context, Poll, Waker};
-/// Start the event loop
-pub fn block_on(fut: Fut) -> Fut::Output
+/// Start the event loop. Blocks until the future
+pub fn block_on(fut: F) -> F::Output
where
- Fut: Future,
+ F: Future + 'static,
+ F::Output: 'static,
{
// Construct the reactor
let reactor = Reactor::new();
@@ -19,67 +18,52 @@ where
panic!("cannot wstd::runtime::block_on inside an existing block_on!")
}
- // Pin the future so it can be polled
- let mut fut = pin!(fut);
+ // Spawn the task onto the reactor.
+ let root_task = reactor.spawn(fut);
- // Create a new context to be passed to the future.
- let root = Arc::new(RootWaker::new());
- let waker = Waker::from(root.clone());
- let mut cx = Context::from_waker(&waker);
+ loop {
+ match reactor.pop_ready_list() {
+ None => {
+ if reactor.pending_pollables_is_empty() {
+ break;
+ } else {
+ reactor.block_on_pollables()
+ }
+ }
+ Some(runnable) => {
+ // Run the task popped from the head of the runlist. If the
+ // task re-inserts itself onto the runlist during execution,
+ // last_run_awake is a hint that guarantees us the runlist is
+ // nonempty.
+ let last_run_awake = runnable.run();
- // Either the future completes and we return, or some IO is happening
- // and we wait.
- let res = loop {
- match fut.as_mut().poll(&mut cx) {
- Poll::Ready(res) => break res,
- Poll::Pending => {
- // If some non-pollable based future has marked the root task
- // as awake, reset and poll again. otherwise, block until a
- // pollable wakes a future.
- if root.is_awake() {
+ // If any task is ready for running, we perform a nonblocking
+ // check of pollables, giving any tasks waiting on a pollable
+ // a chance to wake.
+ if last_run_awake || !reactor.ready_list_is_empty() {
reactor.nonblock_check_pollables();
- root.reset()
- } else {
- // If there are no futures awake or waiting on a WASI
- // pollable, its impossible for the reactor to make
- // progress, and the only valid behaviors are to sleep
- // forever or panic. This should only be reachable if the
- // user's Futures are implemented incorrectly.
- if !reactor.nonempty_pending_pollables() {
- panic!("reactor has no futures which are awake, or are waiting on a WASI pollable to be ready")
- }
+ } else if !reactor.pending_pollables_is_empty() {
+ // If the runlist is empty, block until any of the pending
+ // pollables have woken a task, putting it back on the
+ // ready list
reactor.block_on_pollables()
+ } else {
+ break;
}
}
}
- };
+ }
// Clear the singleton
REACTOR.replace(None);
- res
-}
-
-/// This waker is used in the Context of block_on. If a Future executing in
-/// the block_on calls context.wake(), it sets this boolean state so that
-/// block_on's Future is polled again immediately, rather than waiting for
-/// an external (WASI pollable) event before polling again.
-struct RootWaker {
- wake: AtomicBool,
-}
-impl RootWaker {
- fn new() -> Self {
- Self {
- wake: AtomicBool::new(false),
+ // Get the result out of the root task
+ let mut root_task = pin!(root_task);
+ let mut noop_context = Context::from_waker(Waker::noop());
+ match root_task.as_mut().poll(&mut noop_context) {
+ Poll::Ready(res) => res,
+ Poll::Pending => {
+ unreachable!(
+ "ready list empty, therefore root task should be ready. malformed root task?"
+ )
}
}
- fn is_awake(&self) -> bool {
- self.wake.load(Ordering::Relaxed)
- }
- fn reset(&self) {
- self.wake.store(false, Ordering::Relaxed);
- }
-}
-impl Wake for RootWaker {
- fn wake(self: Arc) {
- self.wake.store(true, Ordering::Relaxed);
- }
}
diff --git a/src/runtime/reactor.rs b/src/runtime/reactor.rs
index efa1d80..e6e75fc 100644
--- a/src/runtime/reactor.rs
+++ b/src/runtime/reactor.rs
@@ -1,11 +1,12 @@
use super::REACTOR;
+use async_task::{Runnable, Task};
use core::cell::RefCell;
-use core::future;
+use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll, Waker};
use slab::Slab;
-use std::collections::HashMap;
+use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use wasi::io::poll::Pollable;
@@ -68,7 +69,7 @@ pub struct WaitFor {
waitee: Waitee,
needs_deregistration: bool,
}
-impl future::Future for WaitFor {
+impl Future for WaitFor {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
let reactor = Reactor::current();
@@ -91,15 +92,16 @@ impl Drop for WaitFor {
/// Manage async system resources for WASI 0.2
#[derive(Debug, Clone)]
pub struct Reactor {
- inner: Rc>,
+ inner: Rc,
}
/// The private, internal `Reactor` implementation - factored out so we can take
/// a lock of the whole.
#[derive(Debug)]
struct InnerReactor {
- pollables: Slab,
- wakers: HashMap,
+ pollables: RefCell>,
+ wakers: RefCell>,
+ ready_list: RefCell>,
}
impl Reactor {
@@ -119,18 +121,19 @@ impl Reactor {
/// Create a new instance of `Reactor`
pub(crate) fn new() -> Self {
Self {
- inner: Rc::new(RefCell::new(InnerReactor {
- pollables: Slab::new(),
- wakers: HashMap::new(),
- })),
+ inner: Rc::new(InnerReactor {
+ pollables: RefCell::new(Slab::new()),
+ wakers: RefCell::new(HashMap::new()),
+ ready_list: RefCell::new(VecDeque::new()),
+ }),
}
}
/// 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 nonempty_pending_pollables(&self) -> bool {
- !self.inner.borrow().wakers.is_empty()
+ pub(crate) fn pending_pollables_is_empty(&self) -> bool {
+ self.inner.wakers.borrow().is_empty()
}
/// Block until at least one pending pollable is ready, waking a pending future.
@@ -152,7 +155,7 @@ impl Reactor {
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.nonempty_pending_pollables() {
+ if self.pending_pollables_is_empty() {
return;
}
// Lazily create a pollable which always resolves to ready.
@@ -186,7 +189,8 @@ impl Reactor {
where
F: FnOnce(&[&Pollable]) -> Vec,
{
- let reactor = self.inner.borrow();
+ let wakers = self.inner.wakers.borrow();
+ let pollables = self.inner.pollables.borrow();
// 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
@@ -194,12 +198,12 @@ impl Reactor {
// We start by iterating over the pollables, and keeping note of which
// pollable belongs to which waker
- let mut indexed_wakers = Vec::with_capacity(reactor.wakers.len());
- let mut targets = Vec::with_capacity(reactor.wakers.len());
- for (waitee, waker) in reactor.wakers.iter() {
+ 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(&reactor.pollables[pollable_index.0]);
+ targets.push(&pollables[pollable_index.0]);
}
// Now that we have that association, we're ready to check our targets for readiness.
@@ -221,33 +225,64 @@ impl Reactor {
/// Turn a Wasi [`Pollable`] into an [`AsyncPollable`]
pub fn schedule(&self, pollable: Pollable) -> AsyncPollable {
- let mut reactor = self.inner.borrow_mut();
- let key = EventKey(reactor.pollables.insert(pollable));
+ let mut pollables = self.inner.pollables.borrow_mut();
+ let key = EventKey(pollables.insert(pollable));
AsyncPollable(Rc::new(Registration { key }))
}
fn deregister_event(&self, key: EventKey) {
- let mut reactor = self.inner.borrow_mut();
- reactor.pollables.remove(key.0);
+ let mut pollables = self.inner.pollables.borrow_mut();
+ pollables.remove(key.0);
}
fn deregister_waitee(&self, waitee: &Waitee) {
- let mut reactor = self.inner.borrow_mut();
- reactor.wakers.remove(waitee);
+ let mut wakers = self.inner.wakers.borrow_mut();
+ wakers.remove(waitee);
}
fn ready(&self, waitee: &Waitee, waker: &Waker) -> bool {
- let mut reactor = self.inner.borrow_mut();
- let ready = reactor
+ let ready = self
+ .inner
.pollables
+ .borrow()
.get(waitee.pollable.0.key.0)
.expect("only live EventKey can be checked for readiness")
.ready();
if !ready {
- reactor.wakers.insert(waitee.clone(), waker.clone());
+ self.inner
+ .wakers
+ .borrow_mut()
+ .insert(waitee.clone(), waker.clone());
}
ready
}
+
+ /// Spawn a `Task` on the `Reactor`.
+ pub fn spawn(&self, fut: F) -> Task
+ where
+ F: Future