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