Skip to main content

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}