Skip to main content

proxima_runtime/
primitives.rs

1//! `RuntimeFactory` sibling trait + the `*Like` primitive shapes.
2//!
3//! Design constraint: [`Runtime`] stays dyn-compatible because the
4//! runtime threads `&dyn Runtime` through per-core dispatch, and GAT
5//! associated-type factories on it would break that workspace-wide.
6//! Resolution: a SIBLING trait `RuntimeFactory: Runtime` carries the
7//! typed primitive factories. `TokioPerCoreRuntime` and `PrimeRuntime`
8//! impl BOTH; consumers that need typed Mutex / Notify / JoinSet /
9//! Sleep bound on `R: RuntimeFactory` (which implies `Runtime`).
10
11use core::future::Future;
12use core::ops::DerefMut;
13#[cfg(feature = "std")]
14use std::time::Duration;
15
16#[cfg(all(feature = "alloc", feature = "std"))]
17use crate::Runtime;
18
19/// Async mutex shape. The guard type is borrowed so callers can hold
20/// the lock across an `await` without paying a clone. Implementations
21/// supply the actual lock primitive — `tokio::sync::Mutex<T>` for the
22/// tokio impl, a per-core single-writer lock for prime.
23pub trait MutexLike<T: Send + 'static>: Send + Sync {
24    type Guard<'guard>: DerefMut<Target = T> + Send
25    where
26        Self: 'guard;
27
28    /// Acquire the lock. The returned future resolves when the guard
29    /// is available; the guard holds the lock until dropped.
30    fn lock(&self) -> impl Future<Output = Self::Guard<'_>> + Send;
31}
32
33/// Notification primitive. `notify_one` wakes one waiter; subsequent
34/// `notified().await` resumes when the next signal arrives.
35pub trait NotifyLike: Send + Sync {
36    /// Wake one waiter, or store a pending notification if no waiter
37    /// is currently parked.
38    fn notify_one(&self);
39
40    /// Park until the next `notify_one` signal arrives. Consumes one
41    /// pending notification atomically with parking.
42    fn notified(&self) -> impl Future<Output = ()> + Send + '_;
43}
44
45/// A handle that yields a task's result on `await` and supports
46/// abort. Matches `tokio::task::JoinSet`'s shape.
47pub trait JoinSetLike<T>: Send {
48    /// Spawn a future onto the set. The future is `Send + 'static`
49    /// because it crosses task boundaries.
50    fn spawn<F>(&mut self, future: F)
51    where
52        F: Future<Output = T> + Send + 'static;
53
54    /// Resolve the next completed task, or `None` if the set is
55    /// empty. `JoinError` wraps the panic / cancellation outcome.
56    fn join_next(&mut self) -> impl Future<Output = Option<Result<T, JoinError>>> + Send + '_;
57
58    /// Abort every spawned task. Subsequent `join_next` calls drain
59    /// the cancellation outcomes.
60    fn abort_all(&mut self);
61}
62
63/// Opaque join-failure reason. Wraps the runtime's native error type
64/// (`tokio::task::JoinError` for the tokio impl).
65#[derive(Debug)]
66pub enum JoinError {
67    /// Task panicked.
68    Panicked,
69    /// Task was cancelled (typically via `abort_all`).
70    Cancelled,
71}
72
73/// Sibling extension of [`Runtime`]: typed factory methods for the
74/// per-session sync / task / time primitives. Implemented by every
75/// concrete runtime that ships in the workspace (`TokioPerCoreRuntime`,
76/// `PrimeRuntime`). Consumers bound on `R: RuntimeFactory` when they
77/// need typed primitives; consumers that only need to spawn stay on
78/// `R: Runtime` and the trait stays dyn-compatible.
79#[cfg(all(feature = "alloc", feature = "std"))]
80pub trait RuntimeFactory: Runtime {
81    type Mutex<T: Send + 'static>: MutexLike<T>;
82    type Notify: NotifyLike;
83    type JoinSet<T: Send + 'static>: JoinSetLike<T>;
84    /// `Send` because the awaiter may move across thread boundaries on
85    /// tokio's multi-thread runtime.
86    type Sleep: Future<Output = ()> + Send;
87
88    fn new_mutex<T: Send + 'static>(value: T) -> Self::Mutex<T>;
89    fn new_notify() -> Self::Notify;
90    fn new_join_set<T: Send + 'static>() -> Self::JoinSet<T>;
91    fn sleep(duration: Duration) -> Self::Sleep;
92}
93
94/// Non-Send mutex shape for runtimes that pin tasks to a
95/// single core. The guard is `!Send` so callers can't accidentally
96/// move it across cores; in exchange the underlying primitive can
97/// skip the atomic/notification dance a Send Mutex needs.
98///
99/// Implementations are typically `Rc<RefCell<T>>`-shaped plus a
100/// per-core `VecDeque<Waker>` waiter queue. Not exposed on
101/// `RuntimeFactory` because tokio's multi-thread runtime can't
102/// honor the !Send guard contract — `LocalRuntimeFactory` is a
103/// separate opt-in for prime-style per-core-pinned runtimes.
104pub trait LocalMutexLike<T: 'static> {
105    type Guard<'guard>: DerefMut<Target = T>
106    where
107        Self: 'guard;
108
109    /// Acquire the lock. The returned future is `!Send` so it
110    /// can only be polled by the core that spawned it.
111    fn lock(&self) -> impl Future<Output = Self::Guard<'_>>;
112}
113
114/// Non-Send notify shape. Same single-core opt-in contract as
115/// [`LocalMutexLike`].
116pub trait LocalNotifyLike {
117    fn notify_one(&self);
118    fn notified(&self) -> impl Future<Output = ()> + '_;
119}
120
121/// Sibling extension of [`Runtime`] for runtimes that pin
122/// tasks to a single core (prime's per-core executor, embedded
123/// runtimes with no multi-thread executor at all). Provides
124/// non-Send primitive factories that skip the atomic synchronization
125/// a Send-shaped primitive needs.
126///
127/// `TokioPerCoreRuntime` does NOT impl this trait because its
128/// multi-thread runtime can move tasks across cores; the !Send
129/// guards would be a soundness bug. Prime opts in because its
130/// `spawn_on_current_core` keeps the future pinned to one thread.
131///
132/// Perf claim: the Local primitives use `Rc<RefCell<T>>` shape
133/// without any atomic operations on the lock state. **Whether this
134/// is measurably faster than `RuntimeFactory::Mutex<T>` on the
135/// uncontended fast path is a question the bench harness has to
136/// answer**; the API exists so the option can be measured (and so
137/// callers can express the single-core constraint at the type
138/// level).
139#[cfg(all(feature = "alloc", feature = "std"))]
140pub trait LocalRuntimeFactory: Runtime {
141    type LocalMutex<T: 'static>: LocalMutexLike<T>;
142    type LocalNotify: LocalNotifyLike;
143
144    fn new_local_mutex<T: 'static>(value: T) -> Self::LocalMutex<T>;
145    fn new_local_notify() -> Self::LocalNotify;
146}
147
148#[cfg(test)]
149#[allow(clippy::unwrap_used, clippy::expect_used)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn join_error_is_debug_constructible() {
155        // smoke-test the error enum so a future impl doesn't add a
156        // variant that breaks Debug.
157        let _ = format!("{:?}", JoinError::Panicked);
158        let _ = format!("{:?}", JoinError::Cancelled);
159    }
160}