Skip to main content

phantom_protocol/runtime/
mod.rs

1//! Async runtime abstraction (Phase 3.1).
2//!
3//! `phantom_protocol` historically hard-coded `tokio` everywhere: every
4//! `tokio::spawn`, every `tokio::time::sleep`, every `tokio::sync::Mutex`,
5//! every `tokio::net::TcpStream`. That couples the library to a
6//! multi-threaded executor that does not exist on `wasm32-unknown-unknown`
7//! (single-threaded, JS event loop) or on bare-metal embedded targets
8//! (no executor at all without `embassy`/RTIC).
9//!
10//! This module introduces a small trait surface — `Runtime` — that the
11//! rest of the crate can use *in place of* direct tokio calls. The default
12//! implementation, [`TokioRuntime`], preserves today's behavior verbatim.
13//! Follow-up commits will gradually migrate call sites; this commit lands
14//! only the trait + the default impl + tests.
15//!
16//! ## What the trait covers
17//!
18//! - **Task spawning** — `spawn(boxed_future) -> SpawnHandle`. The handle
19//!   exposes a non-blocking `abort()` so callers can cancel the task.
20//! - **Sleep** — `sleep(duration) -> BoxFuture<()>` for delay loops.
21//! - **Monotonic clock** — `now_monotonic() -> Instant` for RTT and
22//!   expiry math.
23//! - **Wall-clock** — `now_wall_clock() -> SystemTime` for cookie buckets
24//!   and timestamp-bound material.
25//!
26//! ## What the trait deliberately does NOT cover (yet)
27//!
28//! - **Channels** — `tokio::sync::mpsc` is portable enough across `tokio`
29//!   and `tokio` derivatives that we keep using it directly.
30//! - **Mutexes** — see above.
31//! - **Network I/O** — this is the [`crate::transport::legs`] trait's job.
32//!   `TcpStream` / `UdpSocket` are leg-impl details, not runtime-level.
33//!
34//! ## Implementations
35//!
36//! | Impl | Status | Target |
37//! | --- | --- | --- |
38//! | [`TokioRuntime`] | ✅ | Linux / macOS / Windows / iOS / Android servers and clients |
39//! | `WasmRuntime`    | ✅ (`cfg(target_arch = "wasm32")`) | `wasm32-unknown-unknown` browsers via `wasm-bindgen-futures` |
40//! | `EmbeddedRuntime` | scaffold | `embassy` / bare metal |
41//!
42//! The non-tokio implementations are scaffolded under future feature
43//! flags; this commit only ships the trait and the tokio default so the
44//! call-site migration can begin against a stable abstraction.
45
46use std::future::Future;
47use std::pin::Pin;
48use std::time::{Duration, Instant, SystemTime};
49
50mod tokio_runtime;
51
52pub use tokio_runtime::TokioRuntime;
53
54#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
55pub mod wasm_runtime;
56
57#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
58pub use wasm_runtime::WasmRuntime;
59
60// Phase 3.1 scaffold — see `embedded_runtime.rs` for what this is and is not
61// good for. Gated on `embedded` + `std` for now; pure-no_std support is a
62// follow-up that also has to refactor the `Runtime` trait off
63// `std::time::{Instant, SystemTime}`.
64#[cfg(all(feature = "embedded", feature = "std"))]
65pub mod embedded_runtime;
66
67#[cfg(all(feature = "embedded", feature = "std"))]
68pub use embedded_runtime::EmbeddedRuntime;
69
70// Section B / B2 — WASI Preview 2 runtime. Available only when the
71// `wasi-leg` feature is enabled AND the build target is a WASI triple
72// (`wasm32-wasi*`) — `cfg(target_os = "wasi")` matches all of
73// `wasm32-wasi`, `wasm32-wasip1`, `wasm32-wasip2`. Host builds and
74// `wasm32-unknown-unknown` never see this module; the `compile_error!`
75// in `core/src/lib.rs` rejects `--features wasi-leg` on the browser
76// target.
77#[cfg(all(feature = "wasi-leg", target_os = "wasi"))]
78pub mod wasi_runtime;
79
80#[cfg(all(feature = "wasi-leg", target_os = "wasi"))]
81pub use wasi_runtime::WasiRuntime;
82
83/// Boxed, owned, `Send` future of unit output — the shape `Runtime::spawn`
84/// and `Runtime::sleep` work in.
85///
86/// `'static` lifetime because spawned futures outlive any borrow at the
87/// call site; `Send` because the default tokio impl is multi-threaded and
88/// the futures cross thread boundaries. On future WASM impls the Send
89/// bound is harmless — single-threaded executors accept Send futures.
90pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
91
92/// Async runtime abstraction.
93///
94/// Every method takes `&self` so a single runtime handle (typically wrapped
95/// in `Arc<dyn Runtime>`) can be shared across spawned tasks.
96pub trait Runtime: Send + Sync + 'static {
97    /// Spawn a future to run on the runtime. The returned [`SpawnHandle`]
98    /// can be `abort()`-ed to request cancellation; dropping the handle
99    /// detaches the task without cancelling it.
100    fn spawn(&self, fut: BoxFuture<()>) -> SpawnHandle;
101
102    /// Yield control for at least `duration`.
103    fn sleep(&self, duration: Duration) -> BoxFuture<()>;
104
105    /// Monotonic instant — strictly non-decreasing across calls on the
106    /// same runtime. Used for RTT measurement, retry timers, and any
107    /// duration arithmetic that must not be affected by wall-clock skew.
108    fn now_monotonic(&self) -> Instant;
109
110    /// Wall-clock time. May jump forward or backward as the system clock
111    /// is adjusted. Used for timestamp-bound material (cookie buckets,
112    /// PoW challenge expiry).
113    fn now_wall_clock(&self) -> SystemTime;
114}
115
116/// Opaque handle to a spawned task. Created by [`Runtime::spawn`].
117///
118/// Calling [`abort`](Self::abort) requests the runtime cancel the task at
119/// the next `.await` point. The cancellation is cooperative — a task that
120/// never yields will run to completion regardless.
121///
122/// Dropping a `SpawnHandle` without calling `abort` detaches the task: it
123/// continues running independently. This matches `tokio::task::JoinHandle`
124/// semantics.
125pub struct SpawnHandle {
126    inner: Box<dyn SpawnHandleInner>,
127}
128
129impl SpawnHandle {
130    /// Build a handle from any inner abort-capable implementation.
131    /// Used by runtime adapters (e.g. [`TokioRuntime`]) to wrap their
132    /// concrete `JoinHandle`-equivalent into the trait object.
133    pub fn from_inner<T: SpawnHandleInner>(inner: T) -> Self {
134        Self {
135            inner: Box::new(inner),
136        }
137    }
138
139    /// Request cancellation of the spawned task. Idempotent.
140    pub fn abort(&self) {
141        self.inner.abort();
142    }
143
144    /// Whether the task has finished (success or cancellation).
145    pub fn is_finished(&self) -> bool {
146        self.inner.is_finished()
147    }
148}
149
150impl std::fmt::Debug for SpawnHandle {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("SpawnHandle")
153            .field("is_finished", &self.is_finished())
154            .finish()
155    }
156}
157
158/// Implementation detail of [`SpawnHandle`]. Runtime adapters implement
159/// this on their concrete handle type.
160pub trait SpawnHandleInner: Send + 'static {
161    fn abort(&self);
162    fn is_finished(&self) -> bool;
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::sync::Arc;
169
170    /// Spawn → sleep → see the side effect.
171    #[tokio::test]
172    async fn tokio_runtime_spawn_and_sleep() {
173        let rt: Arc<dyn Runtime> = Arc::new(TokioRuntime);
174        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
175        let c = counter.clone();
176        let rt_for_task = rt.clone();
177        let handle = rt.spawn(Box::pin(async move {
178            rt_for_task.sleep(Duration::from_millis(10)).await;
179            c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
180        }));
181
182        // Wait long enough for the task to run.
183        rt.sleep(Duration::from_millis(100)).await;
184        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
185        assert!(handle.is_finished());
186    }
187
188    /// Aborting a long-running task must prevent its side effect.
189    #[tokio::test]
190    async fn tokio_runtime_abort_cancels_task() {
191        let rt: Arc<dyn Runtime> = Arc::new(TokioRuntime);
192        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
193        let c = counter.clone();
194        let rt_for_task = rt.clone();
195
196        let handle = rt.spawn(Box::pin(async move {
197            rt_for_task.sleep(Duration::from_secs(60)).await;
198            c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
199        }));
200
201        // Give the task a moment to actually start awaiting the sleep,
202        // then abort. The 60-second sleep will never elapse.
203        rt.sleep(Duration::from_millis(20)).await;
204        handle.abort();
205        rt.sleep(Duration::from_millis(20)).await;
206
207        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 0);
208    }
209
210    /// `now_monotonic` must be non-decreasing within a single thread.
211    #[test]
212    fn monotonic_clock_does_not_go_backwards() {
213        let rt = TokioRuntime;
214        let a = rt.now_monotonic();
215        // Spin for a few microseconds.
216        for _ in 0..1000 {
217            std::hint::black_box(a);
218        }
219        let b = rt.now_monotonic();
220        assert!(b >= a, "monotonic clock went backwards: {:?} → {:?}", a, b);
221    }
222
223    /// `now_wall_clock` returns a `SystemTime` that's at or after the
224    /// UNIX epoch (this is essentially "the host clock is sane").
225    #[test]
226    fn wall_clock_is_after_unix_epoch() {
227        let rt = TokioRuntime;
228        let now = rt.now_wall_clock();
229        assert!(now > SystemTime::UNIX_EPOCH);
230    }
231
232    /// Object-safety check — the trait must be usable as `dyn Runtime`.
233    #[test]
234    fn runtime_is_object_safe() {
235        fn assert_runtime_obj_safe(_: &dyn Runtime) {}
236        let rt = TokioRuntime;
237        assert_runtime_obj_safe(&rt);
238    }
239}