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}