Skip to main content

fdu_core/counters/
alloc.rs

1//! A global allocator wrapper with certified, non-reentrant counter sinks.
2
3#![allow(unsafe_code)]
4
5use std::alloc::{GlobalAlloc, Layout, System};
6
7/// Callbacks used by [`CountingAlloc`].
8///
9/// Fields are private so safe code cannot construct a sink that violates the global
10/// allocator contract. Use [`Self::noop`] or the unsafe [`Self::new`] constructor.
11#[derive(Clone, Copy)]
12pub struct Sinks {
13    alloc: fn(u64),
14    realloc: fn(u64),
15    dealloc: fn(),
16}
17
18impl Sinks {
19    /// Construct callbacks that discard every event.
20    #[must_use]
21    pub const fn noop() -> Self {
22        Self { alloc: |_| {}, realloc: |_| {}, dealloc: || {} }
23    }
24
25    /// Construct a certified sink set.
26    ///
27    /// # Safety
28    ///
29    /// Every callback must remain valid for the program's lifetime and must never
30    /// unwind, allocate, deallocate, acquire a lock, or otherwise re-enter the global
31    /// allocator. It must also remain callable during thread-local destruction. All
32    /// arithmetic in a callback must be non-panicking in every build profile.
33    ///
34    /// ```
35    /// fn allocation(_size: u64) {}
36    /// fn reallocation(_growth: u64) {}
37    /// fn deallocation() {}
38    ///
39    /// // SAFETY: these empty callbacks cannot unwind, allocate, lock, or re-enter.
40    /// let sinks = unsafe {
41    ///     fdu_core::counters::alloc::Sinks::new(allocation, reallocation, deallocation)
42    /// };
43    /// let _allocator =
44    ///     fdu_core::counters::alloc::CountingAlloc::system(sinks);
45    /// ```
46    #[must_use]
47    pub const unsafe fn new(alloc: fn(u64), realloc: fn(u64), dealloc: fn()) -> Self {
48        Self { alloc, realloc, dealloc }
49    }
50}
51
52/// An allocator that forwards every request unchanged and reports it to certified sinks.
53pub struct CountingAlloc<A> {
54    inner: A,
55    sinks: Sinks,
56}
57
58impl CountingAlloc<System> {
59    /// Wrap the system allocator with an already-certified sink set.
60    #[must_use]
61    pub const fn system(sinks: Sinks) -> Self {
62        Self { inner: System, sinks }
63    }
64}
65
66impl<A> CountingAlloc<A> {
67    /// Wrap another allocator with an already-certified sink set.
68    #[must_use]
69    pub const fn wrapping(inner: A, sinks: Sinks) -> Self {
70        Self { inner, sinks }
71    }
72}
73
74/// fdu's sink set, whose callbacks use only guarded TLS cells and relaxed atomics.
75pub(super) const fn fdu_sinks() -> Sinks {
76    // SAFETY: these private functions perform only non-panicking saturating arithmetic,
77    // guarded const TLS access, and relaxed atomic updates. They never format, allocate,
78    // deallocate, lock, or call the allocator.
79    unsafe { Sinks::new(super::record_alloc, super::record_realloc, super::record_dealloc) }
80}
81
82// SAFETY: every request is forwarded to the inner allocator with identical arguments
83// and its pointer is returned unchanged. `Sinks` can contain non-noop callbacks only
84// after an unsafe construction that certifies the allocator-specific requirements.
85unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAlloc<A> {
86    #[inline]
87    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
88        (self.sinks.alloc)(u64::try_from(layout.size()).unwrap_or(u64::MAX));
89        // SAFETY: the caller supplied a valid layout; it is forwarded unchanged.
90        unsafe { self.inner.alloc(layout) }
91    }
92
93    #[inline]
94    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
95        (self.sinks.dealloc)();
96        // SAFETY: the caller guarantees this pointer came from the allocator with this
97        // layout; both are forwarded unchanged.
98        unsafe { self.inner.dealloc(ptr, layout) }
99    }
100
101    #[inline]
102    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
103        let growth = u64::try_from(new_size.saturating_sub(layout.size())).unwrap_or(u64::MAX);
104        (self.sinks.realloc)(growth);
105        // SAFETY: the caller's pointer, layout, and new size are forwarded unchanged.
106        unsafe { self.inner.realloc(ptr, layout, new_size) }
107    }
108
109    #[inline]
110    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
111        (self.sinks.alloc)(u64::try_from(layout.size()).unwrap_or(u64::MAX));
112        // SAFETY: the caller supplied a valid layout; it is forwarded unchanged.
113        unsafe { self.inner.alloc_zeroed(layout) }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use std::sync::atomic::{AtomicU64, Ordering};
120
121    use super::*;
122
123    static ALLOCS: AtomicU64 = AtomicU64::new(0);
124    static BYTES: AtomicU64 = AtomicU64::new(0);
125    static FREES: AtomicU64 = AtomicU64::new(0);
126
127    fn alloc(size: u64) {
128        ALLOCS.fetch_add(1, Ordering::Relaxed);
129        BYTES.fetch_add(size, Ordering::Relaxed);
130    }
131
132    fn realloc(growth: u64) {
133        BYTES.fetch_add(growth, Ordering::Relaxed);
134    }
135
136    fn dealloc() {
137        FREES.fetch_add(1, Ordering::Relaxed);
138    }
139
140    fn test_sinks() -> Sinks {
141        // SAFETY: the test callbacks above use relaxed atomics only and cannot unwind,
142        // allocate, lock, or re-enter the allocator.
143        unsafe { Sinks::new(alloc, realloc, dealloc) }
144    }
145
146    #[test]
147    fn forwards_memory_intact_and_counts_it() {
148        let _serial = crate::counters::test_serial();
149        let allocator = CountingAlloc::system(test_sinks());
150        let layout = Layout::from_size_align(64, 8).expect("valid layout");
151        ALLOCS.store(0, Ordering::Relaxed);
152        FREES.store(0, Ordering::Relaxed);
153        BYTES.store(0, Ordering::Relaxed);
154
155        // SAFETY: the valid allocation is checked, used within its bounds, and freed once
156        // with the same allocator and layout.
157        unsafe {
158            let pointer = allocator.alloc(layout);
159            assert!(!pointer.is_null());
160            pointer.write_bytes(0xAB, 64);
161            assert_eq!(pointer.read(), 0xAB);
162            allocator.dealloc(pointer, layout);
163        }
164
165        assert_eq!(ALLOCS.load(Ordering::Relaxed), 1);
166        assert_eq!(FREES.load(Ordering::Relaxed), 1);
167        assert_eq!(BYTES.load(Ordering::Relaxed), 64);
168    }
169
170    #[test]
171    fn noop_sinks_are_const_constructible() {
172        const ALLOCATOR: CountingAlloc<System> = CountingAlloc::system(Sinks::noop());
173        let layout = Layout::from_size_align(16, 8).expect("valid layout");
174        // SAFETY: the valid allocation is freed once with the same allocator and layout.
175        unsafe {
176            let pointer = ALLOCATOR.alloc(layout);
177            assert!(!pointer.is_null());
178            ALLOCATOR.dealloc(pointer, layout);
179        }
180    }
181}