gc_lite/mem.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright (c) 2025-2026 John Ray <996351336@qq.com>
3
4use std::{alloc::Layout, marker::PhantomData, ptr::NonNull};
5
6use crate::{
7 GcError, GcHead, GcHeap, GcNode, GcPartitionId, GcRef,
8 gctype::{layout_align_of, layout_size_of, payload_offset_of},
9 unlikely,
10 weak::GcWeakRawId,
11};
12
13impl GcHeap {
14 fn mem_alloc(&mut self, layout: Layout) -> Option<NonNull<u8>> {
15 debug_assert_ne!(layout.size(), 0, "mem_alloc: zero-sized layout");
16 unsafe {
17 let ptr = std::alloc::alloc(layout);
18
19 if !ptr.is_null() {
20 #[cfg(debug_assertions)]
21 {
22 let n = NonNull::new_unchecked(ptr).cast::<GcHead>();
23 debug_assert!(
24 !self.dbg_living_nodes.contains(&n),
25 "node {ptr:?} already exists"
26 );
27 self.dbg_living_nodes.insert(n);
28 }
29
30 Some(NonNull::new_unchecked(ptr))
31 } else {
32 None
33 }
34 }
35 }
36
37 pub(crate) fn mem_dealloc(&mut self, ptr: NonNull<u8>, layout: Layout) {
38 debug_assert_ne!(layout.size(), 0, "mem_dealloc: zero-sized layout");
39
40 #[cfg(debug_assertions)]
41 debug_assert!(
42 self.dbg_living_nodes.contains(&ptr.cast()),
43 "[O.o][dealloc] bad pointer {ptr:?}"
44 );
45
46 unsafe {
47 #[cfg(debug_assertions)]
48 self.dbg_living_nodes.remove(&ptr.cast());
49
50 std::alloc::dealloc(ptr.as_ptr(), layout);
51 }
52 }
53
54 /// Allocate a typed gc node with payload data.
55 ///
56 /// # Parameters
57 ///
58 /// - `skip_arena`: when `true`, bypasses the arena entirely and goes
59 /// directly to system malloc. Used for root nodes which are permanent
60 /// and should not waste arena space.
61 #[allow(unused_variables)]
62 unsafe fn alloc_node_mem<T: GcNode>(
63 &mut self,
64 partition_id: GcPartitionId,
65 payload: T,
66 skip_arena: bool,
67 ) -> Result<(NonNull<GcHead>, usize), (GcError, T)> {
68 // Early bound check: ensure partition exists
69 if self.partition(partition_id).is_none() {
70 return Err((GcError::PartitionNotFound, payload));
71 }
72
73 let layout = match Layout::from_size_align(layout_size_of::<T>(), layout_align_of::<T>()) {
74 Ok(layout) => layout,
75 Err(_) => return Err((GcError::AllocationFailed, payload)),
76 };
77
78 // gross_size equals `GcTypeInfo::layout_size` for this type,
79 // which is set by the `gc_type_table_internal` macro via the same
80 // `layout_size_of::<T>()` call. This ensures alloc/dealloc symmetry.
81 let gross_size = layout.size();
82
83 if unlikely(
84 self.memory_limit > 0 && self.total_memory_used + gross_size > self.memory_limit,
85 ) {
86 return Err((GcError::PartitionFull, payload));
87 }
88
89 let gc_type = T::GC_TYPE_ID;
90
91 // ── Arena allocation path ────────────────────────────────────────
92 // Arena is a bump-pointer accelerator for small objects only.
93 // When full we fall through directly to system malloc — arena full
94 // does NOT trigger GC (GC is driven by gc_threshold or user calls).
95 #[cfg(feature = "gc_arena")]
96 if !skip_arena {
97 let par = &self.partitions[partition_id.0 as usize];
98 if gross_size <= par.arena_max_alloc {
99 if let Some(ref arena) = par.arena {
100 if let Some(arena_ptr) = arena.alloc(layout) {
101 let head = arena_ptr.cast::<GcHead>();
102
103 // Write payload
104 unsafe {
105 std::ptr::write(
106 arena_ptr.add(payload_offset_of::<T>()).cast::<T>().as_ptr(),
107 payload,
108 );
109 }
110
111 let mut attrs = 0xFF00_0000 | ((gc_type as u32) << 8);
112 attrs |= crate::node::GcNodeFlag::ARENA_ALLOC.bits() as u32;
113
114 let node_info = GcHead {
115 attrs,
116 partition: partition_id.0 as u32,
117 weak_id: GcWeakRawId::NULL,
118 next: None,
119
120 #[cfg(debug_assertions)]
121 dbg_string: std::any::type_name::<T>().into(),
122 };
123
124 unsafe {
125 std::ptr::write(head.as_ptr(), node_info);
126 }
127
128 self.update_mem_use(partition_id, gross_size as i32);
129
130 #[cfg(debug_assertions)]
131 unsafe {
132 let n = NonNull::new_unchecked(head.as_ptr().cast());
133 debug_assert!(
134 !self.dbg_living_nodes.contains(&n),
135 "node {head:?} already exists"
136 );
137 self.dbg_living_nodes.insert(n);
138 }
139
140 return Ok((head, gross_size));
141 }
142
143 // Arena full → fall through to system malloc (NO GC)
144 }
145 }
146 }
147
148 // ── System malloc path (also serves as fallback when arena is full) ──
149 let ptr = match self.mem_alloc(layout) {
150 Some(p) => p,
151 None => {
152 return Err((GcError::AllocationFailed, payload));
153 }
154 };
155
156 let head = ptr.cast::<GcHead>();
157
158 // setup node info and data
159 unsafe {
160 std::ptr::write(
161 ptr.add(payload_offset_of::<T>()).cast::<T>().as_ptr(),
162 payload,
163 );
164 }
165
166 let node_info = GcHead {
167 attrs: { 0xFF00_0000 | ((gc_type as u32) << 8) },
168 partition: partition_id.0 as u32,
169 weak_id: GcWeakRawId::NULL,
170 next: None,
171
172 #[cfg(debug_assertions)]
173 dbg_string: std::any::type_name::<T>().into(),
174 };
175
176 unsafe {
177 std::ptr::write(head.as_ptr(), node_info);
178 }
179
180 self.update_mem_use(partition_id, gross_size as i32);
181
182 Ok((head, gross_size))
183 }
184
185 /// Allocate a typed gc node with payload data, do not put to any scope, even if the current scope is present.
186 ///
187 /// # SAFETY
188 ///
189 /// This function is unsafe because it directly manipulates raw pointers and memory allocation.
190 /// The caller must ensure that the `partition_id` is valid and that the returned `GcRef` is
191 /// properly managed to avoid memory leaks or use-after-free errors.
192 pub unsafe fn alloc_raw<T: GcNode>(
193 &mut self,
194 partition_id: GcPartitionId,
195 payload: T,
196 ) -> Result<GcRef<T>, (GcError, T)> {
197 unsafe {
198 self.alloc_node_mem(partition_id, payload, false)
199 .map(|(head_ptr, _)| {
200 log::trace!("[alloc] {:?}", head_ptr.as_ref());
201
202 self.attach_node(partition_id, head_ptr);
203 GcRef {
204 head_ptr,
205 _marker: PhantomData,
206 }
207 })
208 }
209 }
210
211 /// Allocate a root node — bypasses arena directly to system malloc.
212 ///
213 /// Root nodes are permanent and should not waste arena space.
214 ///
215 /// # SAFETY
216 ///
217 /// This function is unsafe because it directly manipulates raw pointers and memory allocation.
218 /// The caller must ensure that the `partition_id` is valid and that the returned `GcRef` is
219 /// properly managed to avoid memory leaks or use-after-free errors.
220 pub unsafe fn alloc_root_raw<T: GcNode>(
221 &mut self,
222 partition_id: GcPartitionId,
223 payload: T,
224 ) -> Result<GcRef<T>, (GcError, T)> {
225 let head_ptr = unsafe {
226 // Root nodes skip arena — they are permanent and should not
227 // consume arena space that would otherwise be reusable.
228 let (mut h, _) = self.alloc_node_mem(partition_id, payload, true)?;
229 h.as_mut().insert_flag(crate::node::GcNodeFlag::ROOT);
230 log::trace!("[alloc_root] {:?}", h.as_ref());
231 h
232 };
233
234 self.attach_node(partition_id, head_ptr);
235
236 let par = self.partition_mut(partition_id).unwrap();
237 if par.is_marking() {
238 par.add_gray_node(head_ptr);
239 }
240
241 Ok(GcRef {
242 head_ptr,
243 _marker: PhantomData,
244 })
245 }
246
247 /// Dispose a node
248 pub(crate) fn dispose(&mut self, node: NonNull<GcHead>) -> usize {
249 let hd = unsafe { node.as_ref() };
250 log::trace!("[dispose] {hd:?}");
251
252 #[cfg(debug_assertions)]
253 hd.debug_assert_node_valid(self);
254
255 let partition_id = hd.partition_id();
256
257 if !hd.weak_id.is_null() {
258 // clear weak slot
259 let widx = hd.weak_id.index();
260 debug_assert!(
261 (widx as usize) < self.weak_slots.len(),
262 "dispose: weak slot index {} out of bounds (len {})",
263 widx,
264 self.weak_slots.len(),
265 );
266 unsafe {
267 self.weak_slots.get_unchecked_mut(widx as usize).1.set(None);
268 }
269 }
270
271 let dtype = hd.dtype() as usize;
272 let info = &self.node_dtypes.type_info_list[dtype];
273 // Layout comes from `GcTypeInfo::layout_size` / `layout_align`, which are
274 // set by the `gc_type_table_internal` macro via `layout_size_of::<T>()` /
275 // `layout_align_of::<T>()`. This matches the Layout used at allocation time
276 // in `alloc_node_mem`, ensuring alloc/dealloc symmetry.
277 let layout = info.layout();
278 let gross_size = layout.size();
279
280 if let Some(f) = info.drop_fn {
281 unsafe {
282 f(info.payload_ptr(node).as_ptr());
283 }
284 }
285
286 #[cfg(debug_assertions)]
287 unsafe {
288 // Poison GcHead fields so any subsequent use-after-free is caught.
289 // 0xDEAD_BEEF destroys the 0xFF sentinel byte (debug_assert_node_valid_simple
290 // will fail) and is non-zero so even writes that only modify low bits
291 // (e.g. remove_flag) change the value, triggering malloc checksum detection.
292 // Must run AFTER drop_fn so the payload Drop can still access GcHead.
293 (*node.as_ptr()).attrs = 0xDEAD_BEEF;
294 (*node.as_ptr()).next = None;
295 }
296
297 #[cfg(feature = "gc_arena")]
298 {
299 if hd.contains_flag(crate::node::GcNodeFlag::ARENA_ALLOC) {
300 // Arena-allocated node: memory is owned by GcArena, not by
301 // individual system malloc. Skip mem_dealloc — hole collection
302 // (or frontier merge) is handled by sweep.
303 self.update_mem_use(partition_id, -(gross_size as i32));
304
305 #[cfg(debug_assertions)]
306 {
307 self.dbg_living_nodes.remove(&node.cast());
308 }
309
310 return gross_size;
311 }
312 }
313
314 self.mem_dealloc(node.cast::<u8>(), layout);
315
316 // Reclaim memory accounting for both partition and global counters.
317 // Use i32::MAX as a safe upper bound; gross_size is always well below that.
318 self.update_mem_use(partition_id, -(gross_size as i32));
319
320 gross_size
321 }
322
323 /// Call `Drop::drop` on a node's payload without deallocating memory.
324 ///
325 /// This is the Phase 1 operation in the two-phase partition removal design.
326 /// It only calls the type's `drop_fn` on the payload — no memory deallocation,
327 /// no memory accounting update. Those happen in Phase 2 (`dealloc_partition`).
328 ///
329 /// Note: weak_slots are pre-cleared by `finalize_partition` before any drop
330 /// callbacks run, so `GcWeak::upgrade()` of nodes in the same partition will
331 /// return `None` during `Drop` callbacks.
332 ///
333 /// # Safety
334 ///
335 /// - `node` must point to a valid, live GC node managed by `self`.
336 /// - After calling this, the node's payload is considered dropped and must
337 /// not be accessed again (except for deallocation).
338 pub(crate) fn drop_node_payload_without_dealloc(&self, node: NonNull<GcHead>) {
339 let dtype = unsafe { node.as_ref().dtype() } as usize;
340 let info = &self.node_dtypes.type_info_list[dtype];
341 if let Some(f) = info.drop_fn {
342 unsafe {
343 f(info.payload_ptr(node).as_ptr());
344 }
345 }
346 }
347}