Skip to main content

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}