Skip to main content

forge_alloc/backing/
system.rs

1//! `System` — a thin newtype that exposes the process's global allocator
2//! (via [`allocator_api2::alloc::Global`]) as an [`forge_alloc_core::Allocator`].
3//!
4//! Used as a fallback in `WithFallback<Inner, System>` and anywhere a higher
5//! layer needs to defer to the OS heap. Not intended for direct use on hot
6//! paths — the whole point of this library is to avoid the system heap.
7//!
8//! # Implementation note
9//!
10//! This primitive is named after [`std::alloc::System`]. The implementation
11//! routes through [`allocator_api2::alloc::Global`] instead, because `Global`
12//! plugs into both the stable and nightly `Allocator` ABIs through a single
13//! crate. On a default `cargo` build with no `#[global_allocator]` override,
14//! `Global` *is* `std::alloc::System`, so the behaviour is identical. If a
15//! downstream user installs a custom `#[global_allocator]`, `System` here will
16//! route through that allocator — which is consistent with the intent ("fall
17//! back to the OS heap") but worth knowing.
18
19use core::ptr::NonNull;
20
21use allocator_api2::alloc::Allocator as A2;
22use forge_alloc_core::{AllocError, Allocator, Deallocator, NonZeroLayout};
23
24/// Adapter exposing the standard `System` allocator as an `forge_alloc_core::Allocator`.
25///
26/// `System` does NOT implement [`forge_alloc_core::FixedRange`] (it owns no bounded
27/// region) nor [`forge_alloc_core::OsBacked`] (it does not expose page-level controls).
28/// This is intentional — wrappers requiring those traits cannot accidentally
29/// be applied to `System`.
30#[derive(Copy, Clone, Debug, Default)]
31pub struct System;
32
33impl System {
34    /// Construct. ZST — no fields.
35    #[inline]
36    pub const fn new() -> Self {
37        Self
38    }
39}
40
41// We delegate to `allocator_api2::alloc::Global`. On a default cargo build,
42// `Global` resolves to `std::alloc::System`; under a custom
43// `#[global_allocator]`, `Global` resolves to that allocator. Either way the
44// spec's "fall back to the OS heap" contract is honoured.
45//
46// Conversion at the boundary: forge-alloc-core takes `NonZeroLayout`, the std
47// allocator API takes `core::alloc::Layout`. `NonZeroLayout::to_layout()` is
48// infallible.
49
50unsafe impl Deallocator for System {
51    #[inline]
52    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: NonZeroLayout) {
53        // SAFETY: caller upholds Deallocator contract — ptr came from our
54        // allocate(layout), and StdSystem (via the allocator_api2 shim) is
55        // a valid receiver for the corresponding deallocate call.
56        unsafe { A2::deallocate(&allocator_api2::alloc::Global, ptr, layout.to_layout()) }
57    }
58}
59
60unsafe impl Allocator for System {
61    #[inline]
62    fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
63        A2::allocate(&allocator_api2::alloc::Global, layout.to_layout())
64    }
65
66    #[inline]
67    fn allocate_zeroed(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
68        A2::allocate_zeroed(&allocator_api2::alloc::Global, layout.to_layout())
69    }
70
71    #[inline]
72    unsafe fn grow(
73        &self,
74        ptr: NonNull<u8>,
75        old: NonZeroLayout,
76        new: NonZeroLayout,
77    ) -> Result<NonNull<[u8]>, AllocError> {
78        // SAFETY: forwarded; caller upholds Allocator::grow contract.
79        unsafe {
80            A2::grow(
81                &allocator_api2::alloc::Global,
82                ptr,
83                old.to_layout(),
84                new.to_layout(),
85            )
86        }
87    }
88
89    #[inline]
90    unsafe fn shrink(
91        &self,
92        ptr: NonNull<u8>,
93        old: NonZeroLayout,
94        new: NonZeroLayout,
95    ) -> Result<NonNull<[u8]>, AllocError> {
96        // SAFETY: forwarded; caller upholds Allocator::shrink contract.
97        unsafe {
98            A2::shrink(
99                &allocator_api2::alloc::Global,
100                ptr,
101                old.to_layout(),
102                new.to_layout(),
103            )
104        }
105    }
106}
107
108// Sanity check: ensure System is Send + Sync. The std System allocator is.
109const _: () = {
110    fn assert_send<T: Send>() {}
111    fn assert_sync<T: Sync>() {}
112    let _ = assert_send::<System>;
113    let _ = assert_sync::<System>;
114};
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn allocate_then_deallocate() {
122        let s = System;
123        let layout = NonZeroLayout::from_size_align(64, 8).unwrap();
124        let block = s.allocate(layout).expect("System alloc should succeed");
125        let p = block.cast::<u8>();
126        unsafe {
127            core::ptr::write_bytes(p.as_ptr(), 0xAB, 64);
128            assert_eq!(*p.as_ptr(), 0xAB);
129            s.deallocate(p, layout);
130        }
131    }
132
133    #[test]
134    fn allocate_zeroed_returns_zeros() {
135        let s = System;
136        let layout = NonZeroLayout::from_size_align(32, 8).unwrap();
137        let block = s.allocate_zeroed(layout).unwrap();
138        let p = block.cast::<u8>();
139        unsafe {
140            for i in 0..32 {
141                assert_eq!(*p.as_ptr().add(i), 0, "byte {i} should be zeroed");
142            }
143            s.deallocate(p, layout);
144        }
145    }
146
147    #[test]
148    fn capacity_bytes_is_none() {
149        // System is unbounded.
150        assert_eq!(System.capacity_bytes(), None);
151    }
152}