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 pub fn new() -> Heap {
38 Heap {
39 hb: rusty_alloc::init::create_heap(0, false, -1),
40 destroy_on_drop: false,
41 }
42 }
43
44 /// New heap; dropped ⇒ every allocation is released wholesale
45 /// (arena-style teardown).
46 pub fn new_destroyable() -> Heap {
47 Heap {
48 hb: rusty_alloc::init::create_heap(0, true, -1),
49 destroy_on_drop: true,
50 }
51 }
52
53 /// Allocate `layout`, borrowing the heap (so the block cannot outlive it).
54 pub fn alloc(&self, layout: core::alloc::Layout) -> Option<core::ptr::NonNull<u8>> {
55 // SAFETY: hb live (we own it), called on the owning thread by the
56 // !Send/!Sync nature of raw-pointer fields.
57 let p = unsafe {
58 if layout.align() <= 8 {
59 rusty_alloc::alloc::heap_malloc(self.hb, layout.size())
60 } else {
61 rusty_alloc::alloc::heap_malloc_aligned_at(
62 self.hb,
63 layout.size(),
64 layout.align(),
65 0,
66 )
67 }
68 };
69 core::ptr::NonNull::new(p)
70 }
71
72 /// Zeroed variant of [`alloc`](Self::alloc).
73 pub fn alloc_zeroed(&self, layout: core::alloc::Layout) -> Option<core::ptr::NonNull<u8>> {
74 // SAFETY: as alloc.
75 let p = unsafe {
76 if layout.align() <= 8 {
77 rusty_alloc::alloc::heap_zalloc(self.hb, layout.size())
78 } else {
79 rusty_alloc::alloc::heap_zalloc_aligned_at(
80 self.hb,
81 layout.size(),
82 layout.align(),
83 0,
84 )
85 }
86 };
87 core::ptr::NonNull::new(p)
88 }
89
90 /// Free a block previously allocated from this heap.
91 ///
92 /// # Safety
93 /// `p` came from this heap's alloc methods and is freed exactly once.
94 pub unsafe fn dealloc(&self, p: core::ptr::NonNull<u8>) {
95 // SAFETY: forwarded contract.
96 unsafe { rusty_alloc::alloc::free(p.as_ptr()) }
97 }
98
99 /// Drain cross-thread frees and retire empty pages.
100 pub fn collect(&self) {
101 // SAFETY: owner thread (see alloc).
102 unsafe { rusty_alloc::alloc::heap_collect(self.hb, true) }
103 }
104}
105
106impl Default for Heap {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl Drop for Heap {
113 fn drop(&mut self) {
114 // SAFETY: we own hb; exactly one of delete/destroy runs, once.
115 unsafe {
116 if self.destroy_on_drop {
117 rusty_alloc::init::heap_destroy(self.hb);
118 } else {
119 rusty_alloc::init::heap_delete(self.hb);
120 }
121 }
122 }
123}
124
125// SAFETY: GlobalAlloc contract — Layout-described allocation/free delegated to
126// the rusty_alloc core, which returns blocks satisfying the layout's size and
127// alignment (natural bins for align ≤ 8; the aligned path otherwise) and
128// accepts any such block back in `free` regardless of which thread frees it
129// (M4: per-thread heaps, no lock — `free` routes by the segment's owner and
130// hands cross-thread blocks to the loom-modeled remote protocol).
131unsafe impl GlobalAlloc for RustyAlloc {
132 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
133 if layout.align() <= 8 {
134 rusty_alloc::alloc::malloc(layout.size())
135 } else {
136 rusty_alloc::alloc::malloc_aligned(layout.size(), layout.align())
137 }
138 }
139
140 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
141 // SAFETY: GlobalAlloc contract — ptr came from `alloc` and is freed once.
142 unsafe { rusty_alloc::alloc::free(ptr) }
143 }
144
145 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
146 if layout.align() <= 8 {
147 rusty_alloc::alloc::zalloc(layout.size())
148 } else {
149 rusty_alloc::alloc::zalloc_aligned(layout.size(), layout.align())
150 }
151 }
152
153 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
154 if layout.align() <= 8 {
155 // SAFETY: GlobalAlloc contract — ptr live, invalidated on move;
156 // our realloc preserves min(old, new) bytes.
157 unsafe { rusty_alloc::alloc::realloc(ptr, new_size) }
158 } else {
159 // Aligned realloc lands in M5; the default alloc-copy-dealloc is
160 // correct through our aligned paths meanwhile.
161 // SAFETY: forwarded GlobalAlloc contract.
162 unsafe {
163 let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
164 let np = GlobalAlloc::alloc(self, new_layout);
165 if !np.is_null() {
166 core::ptr::copy_nonoverlapping(ptr, np, layout.size().min(new_size));
167 GlobalAlloc::dealloc(self, ptr, layout);
168 }
169 np
170 }
171 }
172 }
173}