Skip to main content

rusty_alloc_api/
lib.rs

1//! Safe Rust-native surface of rusty_alloc (plan §5.14).
2//!
3//! From M2, [`RustyAlloc`] is a real [`core::alloc::GlobalAlloc`]:
4//!
5//! ```ignore
6//! #[global_allocator]
7//! static ALLOC: rusty_alloc_api::RustyAlloc = rusty_alloc_api::RustyAlloc;
8//! ```
9//!
10//! `Heap` and the `Allocator` trait impl land at M6. This crate stays a thin
11//! veneer over the same internals as the C ABI — no separate code path, so
12//! corpus numbers speak for Rust users too.
13
14#![cfg_attr(not(test), no_std)]
15#![deny(missing_docs)]
16
17use core::alloc::{GlobalAlloc, Layout};
18
19pub use rusty_alloc::{MI_COMPAT_VERSION, VERSION, version};
20
21/// The global allocator handle (zero-sized).
22pub struct RustyAlloc;
23
24/// A first-class heap (plan §5.14): `Drop` runs `mi_heap_delete` semantics
25/// (blocks migrate to the thread's backing heap and stay valid) unless built
26/// with [`Heap::new_destroyable`], where `Drop` releases every block at once.
27/// The destroyable form inherits C's contract: callers must not touch its
28/// blocks after drop (a lifetime-carrying `Allocator` impl that makes this
29/// unrepresentable is the planned follow-up once allocator_api stabilizes).
30pub struct Heap {
31    hb: *mut rusty_alloc::init::HeapBox,
32    destroy_on_drop: bool,
33}
34
35impl Heap {
36    /// New heap; dropped ⇒ blocks migrate to the backing heap.
37    ///
38    /// # Panics
39    /// When the OS refuses the heap's backing mapping (memory exhaustion) —
40    /// a defined panic, matching std's convention for infallible
41    /// constructors, rather than a null pointer carried into later use.
42    pub fn new() -> Heap {
43        let hb = rusty_alloc::init::create_heap(0, false, -1);
44        assert!(!hb.is_null(), "rusty_alloc: heap creation failed (OOM)");
45        Heap {
46            hb,
47            destroy_on_drop: false,
48        }
49    }
50
51    /// New heap; dropped ⇒ every allocation is released wholesale
52    /// (arena-style teardown).
53    ///
54    /// # Panics
55    /// As [`Heap::new`], on memory exhaustion.
56    pub fn new_destroyable() -> Heap {
57        let hb = rusty_alloc::init::create_heap(0, true, -1);
58        assert!(!hb.is_null(), "rusty_alloc: heap creation failed (OOM)");
59        Heap {
60            hb,
61            destroy_on_drop: true,
62        }
63    }
64
65    /// Allocate `layout`, borrowing the heap (so the block cannot outlive it).
66    pub fn alloc(&self, layout: core::alloc::Layout) -> Option<core::ptr::NonNull<u8>> {
67        // SAFETY: hb live (we own it), called on the owning thread by the
68        // !Send/!Sync nature of raw-pointer fields.
69        let p = unsafe {
70            if layout.align() <= 8 {
71                rusty_alloc::alloc::heap_malloc(self.hb, layout.size())
72            } else {
73                rusty_alloc::alloc::heap_malloc_aligned_at(
74                    self.hb,
75                    layout.size(),
76                    layout.align(),
77                    0,
78                )
79            }
80        };
81        core::ptr::NonNull::new(p)
82    }
83
84    /// Zeroed variant of [`alloc`](Self::alloc).
85    pub fn alloc_zeroed(&self, layout: core::alloc::Layout) -> Option<core::ptr::NonNull<u8>> {
86        // SAFETY: as alloc.
87        let p = unsafe {
88            if layout.align() <= 8 {
89                rusty_alloc::alloc::heap_zalloc(self.hb, layout.size())
90            } else {
91                rusty_alloc::alloc::heap_zalloc_aligned_at(
92                    self.hb,
93                    layout.size(),
94                    layout.align(),
95                    0,
96                )
97            }
98        };
99        core::ptr::NonNull::new(p)
100    }
101
102    /// Free a block previously allocated from this heap.
103    ///
104    /// # Safety
105    /// `p` came from this heap's alloc methods and is freed exactly once.
106    pub unsafe fn dealloc(&self, p: core::ptr::NonNull<u8>) {
107        // SAFETY: forwarded contract.
108        unsafe { rusty_alloc::alloc::free(p.as_ptr()) }
109    }
110
111    /// Drain cross-thread frees and retire empty pages.
112    pub fn collect(&self) {
113        // SAFETY: owner thread (see alloc).
114        unsafe { rusty_alloc::alloc::heap_collect(self.hb, true) }
115    }
116}
117
118impl Default for Heap {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl Drop for Heap {
125    fn drop(&mut self) {
126        // SAFETY: we own hb; exactly one of delete/destroy runs, once.
127        unsafe {
128            if self.destroy_on_drop {
129                rusty_alloc::init::heap_destroy(self.hb);
130            } else {
131                rusty_alloc::init::heap_delete(self.hb);
132            }
133        }
134    }
135}
136
137// SAFETY: GlobalAlloc contract — Layout-described allocation/free delegated to
138// the rusty_alloc core, which returns blocks satisfying the layout's size and
139// alignment (natural bins for align ≤ 8; the aligned path otherwise) and
140// accepts any such block back in `free` regardless of which thread frees it
141// (M4: per-thread heaps, no lock — `free` routes by the segment's owner and
142// hands cross-thread blocks to the loom-modeled remote protocol).
143unsafe impl GlobalAlloc for RustyAlloc {
144    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
145        if layout.align() <= 8 {
146            rusty_alloc::alloc::malloc(layout.size())
147        } else {
148            rusty_alloc::alloc::malloc_aligned(layout.size(), layout.align())
149        }
150    }
151
152    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
153        // SAFETY: GlobalAlloc contract — ptr came from `alloc` and is freed once.
154        unsafe { rusty_alloc::alloc::free(ptr) }
155    }
156
157    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
158        if layout.align() <= 8 {
159            rusty_alloc::alloc::zalloc(layout.size())
160        } else {
161            rusty_alloc::alloc::zalloc_aligned(layout.size(), layout.align())
162        }
163    }
164
165    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
166        if layout.align() <= 8 {
167            // SAFETY: GlobalAlloc contract — ptr live, invalidated on move;
168            // our realloc preserves min(old, new) bytes.
169            unsafe { rusty_alloc::alloc::realloc(ptr, new_size) }
170        } else {
171            // Aligned realloc lands in M5; the default alloc-copy-dealloc is
172            // correct through our aligned paths meanwhile.
173            // SAFETY: forwarded GlobalAlloc contract.
174            unsafe {
175                let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
176                let np = GlobalAlloc::alloc(self, new_layout);
177                if !np.is_null() {
178                    core::ptr::copy_nonoverlapping(ptr, np, layout.size().min(new_size));
179                    GlobalAlloc::dealloc(self, ptr, layout);
180                }
181                np
182            }
183        }
184    }
185}