Skip to main content

rivet/
task.rs

1//! Task types, generic future storage, and the task registry.
2//!
3//! Tasks are placed in the `.rivet_tasks` linker section for discovery
4//! by the executor at startup. Each entry is a `TaskReg` — just the
5//! metadata the executor needs to poll a task. The task's actual
6//! `Future` state machine lives in a [`TaskCell`], sized generically
7//! and monomorphized per concrete future type.
8//!
9//! # Why not `static TASK: TaskCell<F> = ...`?
10//!
11//! `F` is the compiler-generated, unnameable type of an `async fn`'s
12//! `Future`. Stable Rust cannot name that type in a `static` declaration
13//! (that requires `type_alias_impl_trait`, nightly-only). Instead,
14//! [`TaskCell`] is generic over a `usize` **byte size**, which *is*
15//! nameable (`TaskCell<512>`), and the actual read/write of the future
16//! happens inside a generic method (`TaskCell::poll::<F>`) that the
17//! compiler monomorphizes separately for each task's concrete `F`. This
18//! gets real `async fn` tasks with static, zero-allocation storage on
19//! 100% stable Rust.
20
21use core::cell::UnsafeCell;
22use core::future::Future;
23use core::mem::MaybeUninit;
24use core::pin::Pin;
25use core::task::{Context, Poll, Waker};
26
27/// Maximum number of tasks per priority level (RIVET_MAX_COOP_TASKS).
28pub const MAX_TASKS: usize = crate::config::MAX_TASKS;
29
30// The waker bitmap is 32-wide per priority; a larger MAX_TASKS would
31// silently truncate indices (plan.md [B12] — a capacity mismatch that
32// used to be a runtime masking bug). Make it a compile error.
33const _: () = assert!(MAX_TASKS <= 32);
34
35/// The unified identity of a cooperative-tier task: `(priority, index)`
36/// packed into one u16, used by the waker, executor, timer queue,
37/// semaphore, and channel (plan.md [B12] — replaces two ad-hoc encodings
38/// `(prio << 24) | index` and `(prio << 8) | index`).
39#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
40pub struct TaskId(u16);
41
42impl TaskId {
43    pub const fn new(priority: u8, index: u8) -> Self {
44        Self(((priority as u16) << 8) | (index as u16))
45    }
46
47    pub fn priority(self) -> u8 {
48        (self.0 >> 8) as u8
49    }
50
51    pub fn index(self) -> u8 {
52        (self.0 & 0xFF) as u8
53    }
54
55    /// Raw u16 encoding (used by [`crate::executor::current_task`]).
56    pub const fn as_u16(self) -> u16 {
57        self.0
58    }
59
60    pub const fn from_u16(v: u16) -> Self {
61        Self(v)
62    }
63}
64
65/// Maximum priority level (0 = lowest, 31 = highest).
66pub const MAX_PRIORITY: u8 = (crate::config::PRIORITY_LEVELS - 1) as u8;
67
68/// Default byte size reserved for a task's future state machine when the
69/// user doesn't override it with `#[rivet::task(stack = N)]`.
70pub const DEFAULT_TASK_SIZE: usize = 512;
71
72/// Alignment guaranteed for future storage inside a [`TaskCell`].
73/// Covers all primitive/usize/u64-aligned types used in typical embedded
74/// futures. `TaskCell::poll` asserts this at runtime on first use.
75pub const TASK_CELL_ALIGN: usize = 16;
76
77/// Generic, zero-allocation storage for one task's `Future` state machine.
78///
79/// `SIZE` is a byte count, chosen by `#[rivet::task(stack = SIZE)]`
80/// (default [`DEFAULT_TASK_SIZE`]). The concrete future type is supplied
81/// only when polling, via a monomorphized generic method — this is what
82/// lets the byte size (and therefore the `static` declaration) be nameable
83/// without knowing the future's real type.
84#[repr(C, align(16))]
85pub struct TaskCell<const SIZE: usize> {
86    buf: UnsafeCell<MaybeUninit<[u8; SIZE]>>,
87    initialized: core::sync::atomic::AtomicBool,
88    /// Set once the future has run to completion and been dropped.
89    /// Prevents re-polling a completed task's dropped future (plan.md
90    /// [B10]: a stale waiter registration must not poll a completed task).
91    completed: core::sync::atomic::AtomicBool,
92}
93
94impl<const SIZE: usize> Default for TaskCell<SIZE> {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100// Safety: TaskCell lives in static memory. It is only ever polled from the
101// single-threaded executor loop, which never re-enters a task while it's
102// already being polled, so &TaskCell access is effectively single-threaded.
103unsafe impl<const SIZE: usize> Sync for TaskCell<SIZE> {}
104
105impl<const SIZE: usize> TaskCell<SIZE> {
106    /// Create empty task storage.
107    pub const fn new() -> Self {
108        Self {
109            buf: UnsafeCell::new(MaybeUninit::uninit()),
110            initialized: core::sync::atomic::AtomicBool::new(false),
111            completed: core::sync::atomic::AtomicBool::new(false),
112        }
113    }
114
115    /// Whether this task's future has completed (and been dropped).
116    /// The executor uses this to skip completed tasks and track the
117    /// live-task count (plan.md [B10]).
118    pub fn is_completed(&self) -> bool {
119        self.completed.load(core::sync::atomic::Ordering::Acquire)
120    }
121
122    /// Poll the task's future, creating it on first call via `init`.
123    ///
124    /// `init` is the task's async fn, used as a zero-sized `fn() -> F`
125    /// (so tasks currently take no arguments — shared state goes through
126    /// `static`s, which is also the idiomatic embedded pattern for
127    /// peripherals and shared queues).
128    ///
129    /// # Panics
130    /// Panics if `F` doesn't fit in `SIZE` bytes or needs stricter
131    /// alignment than [`TASK_CELL_ALIGN`]. Increase
132    /// `#[rivet::task(stack = N)]` if this fires.
133    ///
134    /// # Safety
135    /// Must only ever be called with the *same* `F` on every invocation
136    /// for a given `TaskCell` (true by construction: the proc macro
137    /// generates one non-generic wrapper per task that always calls this
138    /// with the same `init` function).
139    pub unsafe fn poll<F: Future<Output = ()> + 'static>(
140        &self,
141        init: fn() -> F,
142        waker: &Waker,
143    ) -> Poll<()> {
144        assert!(
145            core::mem::size_of::<F>() <= SIZE,
146            "rivet: task future ({} bytes) exceeds reserved stack size ({} bytes); \
147             increase #[rivet::task(stack = N)]",
148            core::mem::size_of::<F>(),
149            SIZE
150        );
151        assert!(
152            core::mem::align_of::<F>() <= TASK_CELL_ALIGN,
153            "rivet: task future requires stricter alignment than supported"
154        );
155
156        let ptr = self.buf.get() as *mut u8;
157
158        if !self.initialized.load(core::sync::atomic::Ordering::Acquire) {
159            let future = init();
160            core::ptr::write(ptr as *mut F, future);
161            self.initialized
162                .store(true, core::sync::atomic::Ordering::Release);
163        }
164
165        // A completed task is never re-polled — its future has been
166        // dropped (plan.md [B10]).
167        if self.completed.load(core::sync::atomic::Ordering::Acquire) {
168            return Poll::Ready(());
169        }
170
171        let fut: &mut F = &mut *(ptr as *mut F);
172        let pinned = Pin::new_unchecked(fut);
173        let mut cx = Context::from_waker(waker);
174        let result = pinned.poll(&mut cx);
175
176        if result.is_ready() {
177            // Drop the future in place and mark the cell completed so a
178            // stale wake can never poll the dropped state machine.
179            // SAFETY: the future at `ptr` is initialized and we hold the
180            // only reference; dropping it exactly once is sound.
181            unsafe {
182                core::ptr::drop_in_place(ptr as *mut F);
183            }
184            self.completed
185                .store(true, core::sync::atomic::Ordering::Release);
186        }
187        result
188    }
189}
190
191/// Registration entry placed in the `.rivet_tasks` linker section.
192/// The executor walks these to discover all statically-declared tasks.
193#[repr(C)]
194pub struct TaskReg {
195    /// Task priority (0-31).
196    pub priority: u8,
197    /// Index within this priority level (assigned at init time).
198    pub index_in_priority: u8,
199    /// Reserved padding for alignment.
200    pub _reserved: [u8; 2],
201    /// Poll function. Type-erased: internally casts `user_data` back to
202    /// the concrete `TaskCell<SIZE>` and calls `TaskCell::poll::<F>`.
203    pub poll_fn: unsafe fn(user_data: *mut (), waker: &Waker) -> Poll<()>,
204    /// Completed probe. Type-erased: internally casts `user_data` back to
205    /// the concrete `TaskCell<SIZE>` and reports `is_completed()` — lets
206    /// the executor skip completed tasks (plan.md [B10]).
207    pub completed_fn: unsafe fn(user_data: *mut ()) -> bool,
208    /// Opaque pointer to the task's `TaskCell`. Set at compile time.
209    pub user_data: *mut (),
210}
211
212// Safety: TaskReg is placed in static memory (linker section), accessed
213// only by the executor loop (single-threaded).
214unsafe impl Sync for TaskReg {}
215
216/// Convenience macro to declare a task registration by hand (used
217/// internally by `#[rivet::task]`, and available for advanced manual use).
218///
219/// ```ignore
220/// rivet::register_task!(MY_TASK, priority = 1, poll_fn = my_poll, buf = MY_BUF);
221/// ```
222#[macro_export]
223macro_rules! register_task {
224    ($name:ident, priority = $prio:expr, poll_fn = $poll:expr, completed = $completed:expr, buf = $buf:expr) => {
225        #[link_section = ".rivet_tasks"]
226        #[used]
227        static $name: $crate::task::TaskReg = $crate::task::TaskReg {
228            priority: $prio,
229            index_in_priority: 0,
230            _reserved: [0; 2],
231            poll_fn: $poll as unsafe fn(*mut (), &::core::task::Waker) -> ::core::task::Poll<()>,
232            completed_fn: $completed as unsafe fn(*mut ()) -> bool,
233            user_data: unsafe { &raw const $buf as *mut () },
234        };
235    };
236}
237
238/// Runtime task registry built during `Executor::init()`.
239pub(crate) struct TaskRegistry {
240    /// Per-priority array of pointers to TaskReg entries.
241    pub tasks: [[Option<*const TaskReg>; MAX_TASKS]; (MAX_PRIORITY as usize) + 1],
242    /// Number of tasks at each priority level.
243    pub count_per_priority: [u8; (MAX_PRIORITY as usize) + 1],
244    /// Total number of registered tasks.
245    pub total: u8,
246}
247
248impl TaskRegistry {
249    pub const fn new() -> Self {
250        Self {
251            tasks: [[None; MAX_TASKS]; (MAX_PRIORITY as usize) + 1],
252            count_per_priority: [0; (MAX_PRIORITY as usize) + 1],
253            total: 0,
254        }
255    }
256}
257
258// Symbols defined by the linker script.
259extern "C" {
260    static __rivet_tasks_start: u8;
261    static __rivet_tasks_end: u8;
262}
263
264/// Iterate over all TaskReg entries in the `.rivet_tasks` section.
265pub(crate) fn iter_task_regs() -> impl Iterator<Item = &'static TaskReg> {
266    let start = core::ptr::addr_of!(__rivet_tasks_start) as *const TaskReg;
267    let end = core::ptr::addr_of!(__rivet_tasks_end) as *const TaskReg;
268    let count = unsafe {
269        // SAFETY: `__rivet_tasks_start`/`__rivet_tasks_end` are linker
270        // symbols bracketing the `.rivet_tasks` section; both point into
271        // the same allocation, so `offset_from` is well-defined.
272        end.offset_from(start)
273    };
274    let count = if count < 0 { 0 } else { count as usize };
275    (0..count).map(move |i| unsafe {
276        // SAFETY: `i` is bounded by `count`, the number of `TaskReg`
277        // entries measured between the section symbols; each entry is a
278        // `static` that lives for the program's lifetime.
279        &*start.add(i)
280    })
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use core::sync::atomic::{AtomicU32, Ordering};
287
288    static POLL_COUNT: AtomicU32 = AtomicU32::new(0);
289
290    async fn counting_task() {
291        loop {
292            POLL_COUNT.fetch_add(1, Ordering::Relaxed);
293            TestYield { yielded: false }.await;
294        }
295    }
296
297    struct TestYield {
298        yielded: bool,
299    }
300    impl Future for TestYield {
301        type Output = ();
302        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
303            if self.yielded {
304                Poll::Ready(())
305            } else {
306                self.yielded = true;
307                cx.waker().wake_by_ref();
308                Poll::Pending
309            }
310        }
311    }
312
313    #[test]
314    fn task_cell_polls_real_async_fn() {
315        crate::kernel_test! {
316            POLL_COUNT.store(0, Ordering::Relaxed);
317            static CELL: TaskCell<256> = TaskCell::new();
318
319            let waker = crate::waker::task_waker(crate::task::TaskId::new(0, 0));
320            // SAFETY: `CELL` is a fresh static `TaskCell`; both polls use
321            // the same `F` (`counting_task`), as `TaskCell::poll`'s safety
322            // contract requires, and the executor never re-enters.
323            unsafe {
324                let _ = CELL.poll(counting_task, &waker);
325                let _ = CELL.poll(counting_task, &waker);
326            }
327            // Each poll() call drives the loop body once (TestYield yields
328            // once then completes, so the outer `loop` re-enters and
329            // increments again on the *next* poll call after the inner
330            // future completes).
331            assert!(POLL_COUNT.load(Ordering::Relaxed) >= 1);
332        }
333    }
334
335    #[test]
336    #[should_panic(expected = "exceeds reserved stack size")]
337    fn task_cell_panics_when_future_too_large() {
338        crate::kernel_test! {
339            async fn big_task() {
340                // Held live across the await point (read afterward) so the
341                // compiler must keep it in the generated state machine
342                // instead of optimizing away an unread local.
343                let mut buf = [0u8; 1024];
344                buf[0] = 1;
345                TestYield { yielded: false }.await;
346                core::hint::black_box(&buf);
347            }
348            static CELL: TaskCell<8> = TaskCell::new();
349            let waker = crate::waker::task_waker(crate::task::TaskId::new(0, 0));
350            // SAFETY: `CELL` is a fresh static `TaskCell` polled once
351            // with `big_task`; same-`F` contract satisfied.
352            unsafe {
353                let _ = CELL.poll(big_task, &waker);
354            }
355        }
356    }
357}