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}