phantom_protocol/runtime/embedded_runtime.rs
1//! [`EmbeddedRuntime`] — minimal [`Runtime`] implementation for embeddings
2//! that do not (or cannot) carry tokio (Phase 3.1 scaffold).
3//!
4//! **Scaffold status.** This implementation exists to prove the `Runtime`
5//! trait is implementable off-tokio without dragging in a specific
6//! executor crate as a hard dependency. It uses **one OS thread per
7//! spawned future** plus `futures::executor::block_on` inside that
8//! thread to drive the future to completion. That's correct (every
9//! future makes progress, `abort` actually cancels, sleeps actually
10//! wait) but it is **not** what bare-metal embedded targets want —
11//! `embassy_executor`, `RTIC`, or an in-house cooperative scheduler is.
12//! Production embedders are expected to ship their own `impl Runtime`;
13//! the value of this scaffold is that the *trait surface* is now
14//! demonstrably usable off the default tokio impl, so future executor
15//! crates plug in without changing call sites.
16//!
17//! The module is gated on **both** the `embedded` and `std` features.
18//! Pure `no_std` support requires the `Runtime` trait to drop its
19//! `std::time::{Instant, SystemTime}` dependence — tracked as a
20//! follow-up under the same Phase 3.1 work.
21//!
22//! ## What this is good for
23//!
24//! - Wiring up call sites that take `Arc<dyn Runtime>` so they can be
25//! exercised in non-tokio contexts (e.g. host-side integration tests
26//! for the `EmbeddedLeg` transport).
27//! - Demonstrating cancel-safety (`abort` actually drops the spawned
28//! thread's `block_on` future and runs its `Drop` impls).
29//!
30//! ## What this is NOT good for
31//!
32//! - Bare-metal targets (`thumbv7em-none-eabihf`, etc.) — they have no
33//! `std::thread`. They need an `EmbassyRuntime` / `RticRuntime` /
34//! custom-scheduler impl. The follow-up PR ships those alongside the
35//! no_std refactor of the trait.
36//! - High-task-count workloads. One OS thread per future is fine for a
37//! handful of long-lived tasks (handshake driver, accept loop) but a
38//! poor match for short-lived spawned work.
39
40use std::future::Future;
41use std::pin::Pin;
42use std::sync::{Arc, Mutex};
43use std::task::{Context, Poll, Waker};
44use std::thread::{self, JoinHandle};
45use std::time::{Duration, Instant, SystemTime};
46
47use super::{BoxFuture, Runtime, SpawnHandle, SpawnHandleInner};
48
49/// [`Runtime`] impl that spawns one OS thread per future and drives it
50/// with `futures::executor::block_on`. See module docs for limitations.
51#[derive(Clone, Copy, Default)]
52pub struct EmbeddedRuntime;
53
54impl Runtime for EmbeddedRuntime {
55 fn spawn(&self, fut: BoxFuture<()>) -> SpawnHandle {
56 // Each spawned future gets its own OS thread + its own
57 // `block_on`. The shared `aborted` flag lets `SpawnHandle::abort`
58 // signal cancellation cooperatively — the wrapping future polls
59 // it between awaits.
60 let aborted = Arc::new(std::sync::atomic::AtomicBool::new(false));
61 let finished = Arc::new(std::sync::atomic::AtomicBool::new(false));
62
63 let aborted_for_task = Arc::clone(&aborted);
64 let finished_for_task = Arc::clone(&finished);
65
66 let handle = thread::spawn(move || {
67 let wrapped = AbortableFuture {
68 inner: fut,
69 aborted: aborted_for_task,
70 };
71 futures::executor::block_on(wrapped);
72 finished_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
73 });
74
75 SpawnHandle::from_inner(EmbeddedSpawnHandle {
76 handle: Some(handle),
77 aborted,
78 finished,
79 })
80 }
81
82 fn sleep(&self, duration: Duration) -> BoxFuture<()> {
83 Box::pin(SleepFuture::new(duration))
84 }
85
86 fn now_monotonic(&self) -> Instant {
87 Instant::now()
88 }
89
90 fn now_wall_clock(&self) -> SystemTime {
91 SystemTime::now()
92 }
93}
94
95/// Inner type behind a [`SpawnHandle`] produced by [`EmbeddedRuntime`].
96pub(super) struct EmbeddedSpawnHandle {
97 // `Option` so a future `Drop` impl could join/detach if we wanted;
98 // today we leave the thread to its block_on naturally.
99 handle: Option<JoinHandle<()>>,
100 aborted: Arc<std::sync::atomic::AtomicBool>,
101 finished: Arc<std::sync::atomic::AtomicBool>,
102}
103
104impl SpawnHandleInner for EmbeddedSpawnHandle {
105 /// Best-effort cooperative cancellation. **Observed only on the
106 /// next poll wake.**
107 ///
108 /// Sets the shared abort flag; the wrapping `AbortableFuture`
109 /// observes it on its next `poll` and returns `Poll::Ready(())`.
110 /// Until something wakes the task (an awaited timer, channel send,
111 /// or external event), the OS thread driving the future stays
112 /// parked inside `block_on`. Per-future parker threads created by
113 /// `SleepFuture::poll` are NOT cancelled — they keep their
114 /// original `thread::sleep` and then fire a now-useless wake.
115 ///
116 /// In other words: `abort` flips a flag, it does not interrupt.
117 /// This is acceptable for the scaffold's intended use (host-side
118 /// integration tests, demonstrating cancel-safety contracts) but
119 /// is one reason production embedders should ship an
120 /// `EmbassyRuntime` / `RticRuntime` instead of using this impl.
121 fn abort(&self) {
122 self.aborted
123 .store(true, std::sync::atomic::Ordering::SeqCst);
124 }
125
126 fn is_finished(&self) -> bool {
127 self.finished.load(std::sync::atomic::Ordering::SeqCst)
128 || self
129 .handle
130 .as_ref()
131 .map(|h| h.is_finished())
132 .unwrap_or(false)
133 }
134}
135
136// ─── Internal: abortable future wrapper ──────────────────────────────────
137
138struct AbortableFuture {
139 inner: BoxFuture<()>,
140 aborted: Arc<std::sync::atomic::AtomicBool>,
141}
142
143impl Future for AbortableFuture {
144 type Output = ();
145
146 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
147 if self.aborted.load(std::sync::atomic::Ordering::SeqCst) {
148 return Poll::Ready(());
149 }
150 self.inner.as_mut().poll(cx)
151 }
152}
153
154// ─── Internal: sleep future ──────────────────────────────────────────────
155
156struct SleepFuture {
157 deadline: Instant,
158 inner: Arc<Mutex<SleepInner>>,
159}
160
161struct SleepInner {
162 waker: Option<Waker>,
163 waiter_spawned: bool,
164}
165
166impl SleepFuture {
167 fn new(duration: Duration) -> Self {
168 Self {
169 deadline: Instant::now() + duration,
170 inner: Arc::new(Mutex::new(SleepInner {
171 waker: None,
172 waiter_spawned: false,
173 })),
174 }
175 }
176}
177
178impl Future for SleepFuture {
179 type Output = ();
180
181 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
182 if Instant::now() >= self.deadline {
183 return Poll::Ready(());
184 }
185
186 // PANIC-SAFETY: the mutex is private to this `SleepFuture` and
187 // the parker thread, neither of which panics while holding it.
188 // A `PoisonError` here would indicate an unrecoverable runtime
189 // bug, not adversary input.
190 #[allow(clippy::expect_used)]
191 let mut inner = self.inner.lock().expect("SleepFuture mutex poisoned");
192 inner.waker = Some(cx.waker().clone());
193
194 // First poll: spawn a parker that wakes us when the deadline
195 // elapses. Subsequent polls just refresh the waker slot above
196 // (the parker holds a clone of the Arc and re-fetches the
197 // current waker when it fires).
198 if !inner.waiter_spawned {
199 inner.waiter_spawned = true;
200 let deadline = self.deadline;
201 let inner_for_parker = Arc::clone(&self.inner);
202 thread::spawn(move || {
203 let now = Instant::now();
204 if deadline > now {
205 thread::sleep(deadline - now);
206 }
207 if let Ok(guard) = inner_for_parker.lock() {
208 if let Some(w) = guard.waker.as_ref() {
209 w.wake_by_ref();
210 }
211 }
212 });
213 }
214
215 Poll::Pending
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::sync::atomic::{AtomicU32, Ordering};
223
224 /// Spawn → see the side effect after a sleep.
225 #[test]
226 fn spawn_and_sleep_round_trip() {
227 let rt: Arc<dyn Runtime> = Arc::new(EmbeddedRuntime);
228 let counter = Arc::new(AtomicU32::new(0));
229 let c = counter.clone();
230 let rt_for_task = rt.clone();
231 let handle = rt.spawn(Box::pin(async move {
232 rt_for_task.sleep(Duration::from_millis(10)).await;
233 c.fetch_add(1, Ordering::SeqCst);
234 }));
235
236 // Block on a sleep here in the test thread until the spawned
237 // task should have run.
238 futures::executor::block_on(rt.sleep(Duration::from_millis(100)));
239 assert_eq!(counter.load(Ordering::SeqCst), 1);
240 assert!(handle.is_finished());
241 }
242
243 /// `abort` must short-circuit a long-running task before its side
244 /// effect can land.
245 #[test]
246 fn abort_cancels_task() {
247 let rt: Arc<dyn Runtime> = Arc::new(EmbeddedRuntime);
248 let counter = Arc::new(AtomicU32::new(0));
249 let c = counter.clone();
250 let rt_for_task = rt.clone();
251
252 let handle = rt.spawn(Box::pin(async move {
253 rt_for_task.sleep(Duration::from_secs(60)).await;
254 c.fetch_add(1, Ordering::SeqCst);
255 }));
256
257 // Give the parker thread a moment to spawn, then abort.
258 futures::executor::block_on(rt.sleep(Duration::from_millis(50)));
259 handle.abort();
260 // Give the wrapping AbortableFuture a chance to observe the
261 // flag — but `block_on` won't poll a Pending future without a
262 // wake. Force a wake by sleeping again.
263 futures::executor::block_on(rt.sleep(Duration::from_millis(50)));
264
265 // The increment must not have happened within our short window
266 // (60-second sleep is the upper bound).
267 assert_eq!(counter.load(Ordering::SeqCst), 0);
268 }
269
270 #[test]
271 fn monotonic_clock_does_not_go_backwards() {
272 let rt = EmbeddedRuntime;
273 let a = rt.now_monotonic();
274 for _ in 0..1000 {
275 std::hint::black_box(a);
276 }
277 let b = rt.now_monotonic();
278 assert!(b >= a, "monotonic clock went backwards: {:?} → {:?}", a, b);
279 }
280
281 #[test]
282 fn wall_clock_is_after_unix_epoch() {
283 let rt = EmbeddedRuntime;
284 let now = rt.now_wall_clock();
285 assert!(now > SystemTime::UNIX_EPOCH);
286 }
287
288 /// Object-safety: usable as `dyn Runtime` just like tokio.
289 #[test]
290 fn embedded_runtime_is_object_safe() {
291 fn assert_runtime_obj_safe(_: &dyn Runtime) {}
292 let rt = EmbeddedRuntime;
293 assert_runtime_obj_safe(&rt);
294 }
295}