Skip to main content

nub_rt/
alloc.rs

1//! A bump allocator with an explicit reset.
2//!
3//! Programs that need `alloc` on a freestanding target need a
4//! `#[global_allocator]`, and a bump arena is the cheapest thing that
5//! works: allocation is a pointer add, and a program that runs once and
6//! exits never needs to free.
7//!
8//! [`BumpAlloc::reset`] is what makes the arena re-usable. Without it a
9//! program is single-shot per instance — the second invocation walks
10//! off the end of the arena and the allocation fails, which surfaces as
11//! a guest panic rather than anything legible. That matters for any
12//! caller that invokes the same instance twice, benchmark harnesses
13//! very much included: measuring steady-state execution means running
14//! the same instance repeatedly.
15//!
16//! Resetting is not free of consequences: it invalidates every live
17//! allocation at once. It is only sound at a point where nothing from
18//! the previous invocation is still borrowed — i.e. at an entry point,
19//! before any work begins. [`bump_allocator!`](crate::bump_allocator)
20//! generates a `reset_heap()` for exactly that use.
21
22use core::alloc::{GlobalAlloc, Layout};
23use core::cell::UnsafeCell;
24
25/// A fixed-size bump arena of `N` bytes.
26///
27/// `dealloc` is a no-op; space is reclaimed only by [`reset`](Self::reset).
28pub struct BumpAlloc<const N: usize> {
29    heap: UnsafeCell<[u8; N]>,
30    pos: UnsafeCell<usize>,
31}
32
33// SAFETY: PVM2 programs are single-threaded — the engine runs one
34// instruction stream per instance and there is no way for a guest to
35// create a thread. Without that, the `UnsafeCell` accesses below would
36// need synchronization.
37unsafe impl<const N: usize> Sync for BumpAlloc<N> {}
38
39impl<const N: usize> BumpAlloc<N> {
40    pub const fn new() -> Self {
41        BumpAlloc {
42            heap: UnsafeCell::new([0; N]),
43            pos: UnsafeCell::new(0),
44        }
45    }
46
47    /// Free everything at once, making the arena usable again.
48    ///
49    /// # Safety
50    ///
51    /// Every pointer previously handed out becomes dangling. Call only
52    /// where no allocation from the previous run is still reachable —
53    /// at an entry point, before any work.
54    pub unsafe fn reset(&self) {
55        unsafe { *self.pos.get() = 0 };
56    }
57
58    /// High-water mark, in bytes. Useful for sizing an arena.
59    pub fn used(&self) -> usize {
60        unsafe { *self.pos.get() }
61    }
62}
63
64impl<const N: usize> Default for BumpAlloc<N> {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70unsafe impl<const N: usize> GlobalAlloc for BumpAlloc<N> {
71    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
72        let pos = unsafe { &mut *self.pos.get() };
73        let aligned = (*pos + layout.align() - 1) & !(layout.align() - 1);
74        let next = aligned + layout.size();
75        if next > N {
76            return core::ptr::null_mut();
77        }
78        *pos = next;
79        unsafe { (*self.heap.get()).as_mut_ptr().add(aligned) }
80    }
81
82    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
83}
84
85/// Install a [`BumpAlloc`] of `$bytes` as the program's global
86/// allocator, and define `reset_heap()` beside it.
87///
88/// A macro rather than a static in this crate because
89/// `#[global_allocator]` must be unique per binary: a static here would
90/// force the arena on every program that links `nub-rt`, including the
91/// ones that never allocate.
92///
93/// ```ignore
94/// nub_rt::bump_allocator!(64 * 1024);
95///
96/// #[nub_rt::endpoint(0)]
97/// fn run(_: u64) -> u64 {
98///     reset_heap();          // re-entrant: safe to invoke repeatedly
99///     my_kernel() as u64
100/// }
101/// ```
102///
103/// On host targets it expands to a `reset_heap()` that does nothing, so
104/// the same source builds both ways.
105#[macro_export]
106macro_rules! bump_allocator {
107    ($bytes:expr) => {
108        #[cfg(target_os = "none")]
109        #[global_allocator]
110        static __NUB_RT_HEAP: $crate::alloc::BumpAlloc<{ $bytes }> =
111            $crate::alloc::BumpAlloc::new();
112
113        /// Release everything allocated so far.
114        ///
115        /// Call at the top of an entry point, before any work — at that
116        /// moment nothing from a previous invocation is reachable, which
117        /// is what makes it sound.
118        #[cfg(target_os = "none")]
119        fn reset_heap() {
120            // SAFETY: called at entry, before any allocation of this
121            // invocation exists and after every allocation of the
122            // previous one has gone out of scope.
123            unsafe { __NUB_RT_HEAP.reset() };
124        }
125
126        /// No-op on host: the system allocator needs no reset.
127        #[cfg(not(target_os = "none"))]
128        #[allow(dead_code)]
129        fn reset_heap() {}
130    };
131}