Skip to main content

basic/
basic.rs

1//! End-to-end tour of the default API surface.
2//!
3//! Walks the path a kernel actually takes: declare a `const`-constructed `static`
4//! allocator, hand it a *holey* boot memory map (usable RAM punctured by the
5//! kernel image and an MMIO hole), then allocate and free physical frames. The
6//! second half wraps the same backend in a [`DepotAllocator`] to show that the
7//! wrappers compose over any allocator through the shared traits.
8//!
9//! Run it:
10//!
11//! ```text
12//! cargo run --example basic                  # the allocation walk
13//! cargo run --example basic --features stats # …plus AllocatorStats readouts
14//! ```
15//!
16//! The pool here is a heap allocation standing in for physical RAM; provenance is
17//! recovered with [`with_exposed_provenance_mut`](core::ptr::with_exposed_provenance_mut)
18//! via the `IdentityProv` harness. A real kernel would supply a higher-half
19//! direct-map pointer carrying provenance established once with inline asm, as the
20//! `Provenance` / `RegionInit` docs describe.
21
22use frame_alloc::{
23    CpuId, DepotAllocator, PageSize, PhysRange, PhysicalAllocator, Provenance, RegionInit,
24    SummaryBuddyAllocator,
25};
26use std::alloc::{Layout, alloc, dealloc};
27use std::num::NonZeroUsize;
28use std::ptr;
29use std::ptr::NonNull;
30
31// Scaffolding: stand-ins for what a kernel would already have.
32
33/// The "physical" pool is a heap allocation whose provenance is exposed in
34/// `Region::addr`, so `with_exposed_provenance_mut` can recover it.
35pub struct IdentityProv;
36
37unsafe impl Provenance for IdentityProv {
38    unsafe fn create(phys: usize) -> NonNull<u8> {
39        unsafe { NonNull::new_unchecked(ptr::with_exposed_provenance_mut(phys)) }
40    }
41    unsafe fn destroy<T>(p: NonNull<T>) -> usize {
42        p.addr().get()
43    }
44}
45
46/// An aligned heap allocation standing in for a span of physical RAM.
47pub struct Region {
48    ptr: *mut u8,
49    layout: Layout,
50}
51
52impl Region {
53    pub fn new(bytes: usize, align: usize) -> Self {
54        let layout = Layout::from_size_align(bytes, align).unwrap();
55        let ptr = unsafe { alloc(layout) };
56        assert!(!ptr.is_null(), "region allocation failed");
57        Self { ptr, layout }
58    }
59
60    pub fn addr(&self) -> usize {
61        self.ptr.expose_provenance()
62    }
63}
64
65impl Drop for Region {
66    fn drop(&mut self) {
67        unsafe { dealloc(self.ptr, self.layout) }
68    }
69}
70
71pub fn n(v: usize) -> NonZeroUsize {
72    NonZeroUsize::new(v).expect("non-zero count")
73}
74
75// Configuration:
76
77/// Base frame: 4 KiB.
78const BASE: PageSize = PageSize::from_log2(12);
79/// Order ceiling. Order `k` is a `BASE << k` block, so order 8 is a 1 MiB block.
80const ORDERS: usize = 9;
81const MAX_BLOCK: usize = BASE.bytes() << (ORDERS - 1);
82
83/// Total span the allocator manages, in base frames. 256 × 4 KiB = 1 MiB.
84const SPAN_FRAMES: usize = 256;
85
86/// A `const`-constructed `static`: no runtime initialiser, nothing on the heap.
87/// `init` happens once at boot; from then on it is shared, lock-free for the
88/// fast path, across every CPU.
89static PHYS: SummaryBuddyAllocator<ORDERS, IdentityProv> = SummaryBuddyAllocator::new(BASE);
90
91fn main() {
92    // A boot memory map with holes:
93    //
94    // The backing pool spans the whole window; we then declare only the *usable*
95    // sub-ranges. Two holes stay reserved and are never handed out:
96    //
97    //   [  0 .. 100)  usable   <- the buddy carves its in-pool bitmap from here
98    //   [100 .. 104)  RESERVED  (kernel image)
99    //   [104 .. 200)  usable
100    //   [200 .. 205)  RESERVED  (MMIO window)
101    //   [205 .. 256)  usable
102    let pool = Region::new(SPAN_FRAMES * BASE.bytes(), MAX_BLOCK);
103    let phys_base = pool.addr(); // the "physical" origin
104
105    let frame = BASE.bytes();
106    let range = |lo: usize, hi: usize| PhysRange {
107        base: phys_base + lo * frame,
108        len: (hi - lo) * frame,
109    };
110    let usable = [range(0, 100), range(104, 200), range(205, 256)];
111
112    // SAFETY: single-threaded, called once before any allocation. `phys_base` is
113    // MAX_BLOCK-aligned (Region honours the requested alignment); the bitmap host
114    // range is exclusively owned and reachable through `IdentityProv::create`; the
115    // usable ranges are sorted, non-overlapping, base-frame-aligned, and within
116    // the span.
117    unsafe { PHYS.init(phys_base, SPAN_FRAMES * frame, &usable) };
118
119    println!("Initialised SummaryBuddy over a 1 MiB span with two reserved holes.");
120    print_stats("after init", &PHYS);
121
122    // A few allocate / deallocate cycles:
123    //
124    // One single base frame, then a 4-frame (order-2) contiguous block. Every
125    // address is physical and aligned to the request size.
126    let single = PHYS
127        .allocate_physical(BASE, n(1))
128        .expect("single-frame alloc");
129    let block = PHYS
130        .allocate_physical(BASE, n(4))
131        .expect("4-frame contiguous alloc");
132    println!("\nallocate_physical(1 frame)  -> {single:#x}");
133    println!("allocate_physical(4 frames) -> {block:#x}");
134    print_stats("with 5 frames out", &PHYS);
135
136    // SAFETY: each address came from `allocate_physical` with the same page size
137    // and count and is not used afterwards.
138    unsafe {
139        PHYS.deallocate_physical(BASE, n(1), single);
140        PHYS.deallocate_physical(BASE, n(4), block);
141    }
142    println!("\nfreed both — buddies merge back.");
143    print_stats("after free", &PHYS);
144
145    // Composition: a per-CPU magazine + shared depot over a fresh SummaryBuddy:
146    compose_with_depot();
147}
148
149/// Wrap a `SummaryBuddyAllocator` in a [`DepotAllocator`].
150fn compose_with_depot() {
151    /// Uniprocessor selector: every CPU maps to slot 0. A real kernel returns an
152    /// APIC id / `TPIDR_EL1` here.
153    struct OneCpu;
154    impl CpuId for OneCpu {
155        fn current_cpu() -> usize {
156            0
157        }
158    }
159    const SLOTS: usize = 8; // magazines (>= CPUs you want disjoint)
160
161    // Same const-new composability: the whole stack is one `static`-able value.
162    let mag: DepotAllocator<SummaryBuddyAllocator<ORDERS, IdentityProv>, OneCpu, SLOTS> =
163        DepotAllocator::new(BASE, SummaryBuddyAllocator::new(BASE));
164
165    let pool = Region::new(SPAN_FRAMES * BASE.bytes(), MAX_BLOCK);
166    // SAFETY: as above; here the whole region is usable, so the single-range
167    // convenience applies.
168    unsafe { mag.init_region(pool.addr(), SPAN_FRAMES * BASE.bytes()) };
169
170    println!("\n── DepotAllocator<SummaryBuddyAllocator> ──");
171    let a = mag.allocate_physical(BASE, n(1)).expect("mag alloc");
172    // SAFETY: `a` came from this allocator; freed once, then not reused by us.
173    unsafe { mag.deallocate_physical(BASE, n(1), a) };
174    let b = mag.allocate_physical(BASE, n(1)).expect("mag re-alloc");
175    println!("alloc {a:#x} -> free -> alloc {b:#x}");
176    assert_eq!(a, b, "the magazine should return the just-freed frame");
177    println!("re-alloc returned the cached frame (no backend round-trip).");
178    // SAFETY: final free of a live frame.
179    unsafe { mag.deallocate_physical(BASE, n(1), b) };
180}
181
182// Stats readout:
183
184/// Print [`AllocatorStats`](frame_alloc::AllocatorStats) for `a` under `--features stats`.
185#[cfg(feature = "stats")]
186fn print_stats(label: &str, a: &impl frame_alloc::AllocatorStats) {
187    let kib = |b: usize| b / 1024;
188    println!(
189        "  [{label}] total {} KiB · free {} KiB · largest run {} KiB",
190        kib(a.total_bytes()),
191        kib(a.free_bytes()),
192        kib(a.largest_free_bytes()),
193    );
194}
195
196#[cfg(not(feature = "stats"))]
197fn print_stats<A>(_label: &str, _a: &A) {}