Skip to main content

shape_gc/
ptr.rs

1//! GcPtr<T> — typed pointer to a GC-managed object.
2//!
3//! Wraps a raw pointer to a T that is preceded by a GcHeader at ptr - 8.
4//! GcPtr does NOT implement Drop — the GC handles deallocation.
5//!
6//! ## Platform-specific mark bit encoding
7//!
8//! On platforms with hardware pointer masking, mark bits are stored inline
9//! in the upper pointer bits (zero-cost read/write):
10//! - **ARM64 TBI**: bit 56 (top byte is ignored by hardware)
11//! - **x86-64 LAM57**: bit 57 (masked by Linear Address Masking)
12//! - **Software fallback**: delegates to the GcHeader side table (the header's
13//!   color field) since we cannot store metadata in pointer bits without HW support.
14//!
15//! The inline mark bit is a fast-path optimization for the concurrent marker:
16//! checking `has_mark_bit()` avoids a cache-line fetch of the GcHeader during
17//! the mark phase scan.
18
19use crate::header::GcHeader;
20use std::marker::PhantomData;
21
22/// A pointer to a GC-managed object of type T.
23///
24/// The object is laid out in memory as:
25/// ```text
26/// [GcHeader (8 bytes)][T data (size bytes)]
27///                      ^--- GcPtr points here
28/// ```
29///
30/// GcPtr is Copy — no refcount manipulation. The GC is solely responsible
31/// for determining liveness and reclaiming memory.
32#[repr(transparent)]
33pub struct GcPtr<T> {
34    ptr: *mut T,
35    _marker: PhantomData<T>,
36}
37
38// GcPtr is Copy — no refcounting, GC manages lifetime
39impl<T> Copy for GcPtr<T> {}
40impl<T> Clone for GcPtr<T> {
41    fn clone(&self) -> Self {
42        *self
43    }
44}
45
46impl<T> GcPtr<T> {
47    /// Create a GcPtr from a raw pointer.
48    ///
49    /// # Safety
50    /// The pointer must point to a valid T preceded by a GcHeader at ptr - 8.
51    #[inline(always)]
52    pub unsafe fn from_raw(ptr: *mut T) -> Self {
53        debug_assert!(!ptr.is_null(), "GcPtr::from_raw called with null pointer");
54        Self {
55            ptr,
56            _marker: PhantomData,
57        }
58    }
59
60    /// Get the raw pointer to T.
61    #[inline(always)]
62    pub fn as_ptr(self) -> *mut T {
63        self.ptr
64    }
65
66    /// Get the raw pointer as usize (for NaN-boxing payload).
67    #[inline(always)]
68    pub fn as_usize(self) -> usize {
69        self.ptr as usize
70    }
71
72    /// Get a reference to the GcHeader preceding this object.
73    #[inline(always)]
74    pub fn header(self) -> &'static GcHeader {
75        unsafe {
76            let header_ptr =
77                (self.ptr as *const u8).sub(std::mem::size_of::<GcHeader>()) as *const GcHeader;
78            &*header_ptr
79        }
80    }
81
82    /// Get a mutable reference to the GcHeader preceding this object.
83    ///
84    /// # Safety
85    /// Caller must ensure exclusive access to the header.
86    #[inline(always)]
87    pub unsafe fn header_mut(self) -> &'static mut GcHeader {
88        unsafe {
89            let header_ptr =
90                (self.ptr as *mut u8).sub(std::mem::size_of::<GcHeader>()) as *mut GcHeader;
91            &mut *header_ptr
92        }
93    }
94
95    /// Dereference to get a reference to the managed object.
96    ///
97    /// # Safety
98    /// The object must still be alive (not collected).
99    #[inline(always)]
100    pub unsafe fn deref_gc(self) -> &'static T {
101        unsafe { &*self.ptr }
102    }
103
104    /// Dereference to get a mutable reference to the managed object.
105    ///
106    /// # Safety
107    /// The object must still be alive and caller must have exclusive access.
108    #[inline(always)]
109    pub unsafe fn deref_gc_mut(self) -> &'static mut T {
110        unsafe { &mut *self.ptr }
111    }
112
113    // ── Platform-specific mark bit encoding ─────────────────────────
114
115    /// Bit position for the inline mark bit on ARM64 (TBI — top byte ignore).
116    const MARK_BIT_ARM64: usize = 56;
117
118    /// Bit position for the inline mark bit on x86-64 (LAM57).
119    const MARK_BIT_X86_LAM: usize = 57;
120
121    /// Set the inline mark bit in the pointer, returning a new GcPtr.
122    ///
123    /// On ARM64 TBI the mark lives in bit 56. On x86-64 with LAM it lives in
124    /// bit 57. On software-fallback platforms this is a no-op (the caller must
125    /// use the GcHeader side table instead).
126    #[cfg(target_arch = "aarch64")]
127    #[inline(always)]
128    pub fn with_mark_bit(self) -> Self {
129        // ARM TBI: hardware ignores the top byte — bit 56 is safe metadata
130        Self {
131            ptr: (self.ptr as usize | (1 << Self::MARK_BIT_ARM64)) as *mut T,
132            _marker: PhantomData,
133        }
134    }
135
136    /// Clear the inline mark bit, returning a new GcPtr.
137    #[cfg(target_arch = "aarch64")]
138    #[inline(always)]
139    pub fn clear_mark_bit(self) -> Self {
140        Self {
141            ptr: (self.ptr as usize & !(1 << Self::MARK_BIT_ARM64)) as *mut T,
142            _marker: PhantomData,
143        }
144    }
145
146    /// Check whether the inline mark bit is set.
147    #[cfg(target_arch = "aarch64")]
148    #[inline(always)]
149    pub fn has_mark_bit(self) -> bool {
150        (self.ptr as usize & (1 << Self::MARK_BIT_ARM64)) != 0
151    }
152
153    /// Set the inline mark bit (x86-64: use LAM if available, else no-op).
154    #[cfg(target_arch = "x86_64")]
155    #[inline(always)]
156    pub fn with_mark_bit(self) -> Self {
157        if crate::platform::has_x86_lam() {
158            Self {
159                ptr: (self.ptr as usize | (1 << Self::MARK_BIT_X86_LAM)) as *mut T,
160                _marker: PhantomData,
161            }
162        } else {
163            // Software fallback: no inline mark — caller must use GcHeader
164            self
165        }
166    }
167
168    /// Clear the inline mark bit (x86-64: use LAM if available, else no-op).
169    #[cfg(target_arch = "x86_64")]
170    #[inline(always)]
171    pub fn clear_mark_bit(self) -> Self {
172        if crate::platform::has_x86_lam() {
173            Self {
174                ptr: (self.ptr as usize & !(1 << Self::MARK_BIT_X86_LAM)) as *mut T,
175                _marker: PhantomData,
176            }
177        } else {
178            self
179        }
180    }
181
182    /// Check whether the inline mark bit is set (x86-64: use LAM if available).
183    #[cfg(target_arch = "x86_64")]
184    #[inline(always)]
185    pub fn has_mark_bit(self) -> bool {
186        if crate::platform::has_x86_lam() {
187            (self.ptr as usize & (1 << Self::MARK_BIT_X86_LAM)) != 0
188        } else {
189            // Software fallback: always false — caller must check GcHeader
190            false
191        }
192    }
193
194    /// Set the inline mark bit (generic fallback for non-ARM64 / non-x86-64).
195    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
196    #[inline(always)]
197    pub fn with_mark_bit(self) -> Self {
198        // No hardware pointer masking — side table only
199        self
200    }
201
202    /// Clear the inline mark bit (generic fallback).
203    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
204    #[inline(always)]
205    pub fn clear_mark_bit(self) -> Self {
206        self
207    }
208
209    /// Check whether the inline mark bit is set (generic fallback: always false).
210    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
211    #[inline(always)]
212    pub fn has_mark_bit(self) -> bool {
213        false
214    }
215
216    /// Check the mark state, combining inline pointer bits with the GcHeader
217    /// side table for a definitive answer on any platform.
218    ///
219    /// Returns `true` if either the inline mark bit is set (on HW-masking
220    /// platforms) or the GcHeader color is Gray/Black.
221    #[inline(always)]
222    pub fn is_marked(self) -> bool {
223        if self.has_mark_bit() {
224            return true;
225        }
226        // Fallback: check GcHeader color (always authoritative)
227        let header = self.header();
228        header.color() != crate::header::GcColor::White
229    }
230
231    /// Strip all metadata bits from the pointer, returning the raw address.
232    ///
233    /// Uses the detected masking mode to apply the correct mask.
234    #[inline(always)]
235    pub fn raw_ptr(self) -> *mut T {
236        let mode = crate::platform::cached_masking_mode();
237        crate::platform::mask_ptr(self.ptr as *mut u8, mode) as *mut T
238    }
239
240    /// Convert to an untyped pointer for use in the root set / marker.
241    #[inline(always)]
242    pub fn as_untyped(self) -> *mut u8 {
243        self.ptr as *mut u8
244    }
245
246    /// Create from an untyped pointer.
247    ///
248    /// # Safety
249    /// The pointer must actually point to a valid T with GcHeader prefix.
250    #[inline(always)]
251    pub unsafe fn from_untyped(ptr: *mut u8) -> Self {
252        Self {
253            ptr: ptr as *mut T,
254            _marker: PhantomData,
255        }
256    }
257}
258
259impl<T: std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(f, "GcPtr({:p})", self.ptr)
262    }
263}
264
265impl<T> std::fmt::Pointer for GcPtr<T> {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        std::fmt::Pointer::fmt(&self.ptr, f)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::header::{GcColor, GcHeader};
275
276    #[test]
277    fn test_gc_ptr_is_8_bytes() {
278        assert_eq!(std::mem::size_of::<GcPtr<u64>>(), 8);
279    }
280
281    #[test]
282    fn test_gc_ptr_header_access() {
283        // Simulate a GC allocation: [GcHeader][u64 data]
284        let mut buf = [0u8; 16]; // 8 header + 8 data
285        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
286        unsafe {
287            header_ptr.write(GcHeader::new(1, 8));
288        }
289        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
290        unsafe {
291            data_ptr.write(42);
292        }
293
294        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
295        let header = gc_ptr.header();
296        assert_eq!(header.kind, 1);
297        assert_eq!(header.size, 8);
298
299        let val = unsafe { gc_ptr.deref_gc() };
300        assert_eq!(*val, 42);
301    }
302
303    // ── Mark bit tests ──────────────────────────────────────────────
304
305    #[test]
306    fn test_mark_bit_initial_state() {
307        // On x86-64 without LAM (the common test environment), mark bit
308        // methods are no-ops — has_mark_bit() always returns false.
309        let mut buf = [0u8; 16];
310        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
311        unsafe { header_ptr.write(GcHeader::new(0, 8)) };
312        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
313        unsafe { data_ptr.write(0) };
314
315        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
316        assert!(!gc_ptr.has_mark_bit());
317    }
318
319    #[test]
320    fn test_mark_bit_set_clear_cycle() {
321        let mut buf = [0u8; 16];
322        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
323        unsafe { header_ptr.write(GcHeader::new(0, 8)) };
324        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
325        unsafe { data_ptr.write(0xCAFE) };
326
327        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
328
329        // Set mark bit
330        let marked = gc_ptr.with_mark_bit();
331        // Clear mark bit
332        let cleared = marked.clear_mark_bit();
333
334        // On software fallback (no HW masking), set/clear are no-ops,
335        // so the pointer should be unchanged throughout.
336        #[cfg(target_arch = "x86_64")]
337        {
338            if !crate::platform::has_x86_lam() {
339                // Software fallback: mark bit operations are no-ops
340                assert!(!marked.has_mark_bit());
341                assert!(!cleared.has_mark_bit());
342                assert_eq!(gc_ptr.as_ptr(), marked.as_ptr());
343                assert_eq!(gc_ptr.as_ptr(), cleared.as_ptr());
344            } else {
345                // LAM available: mark bit should work
346                assert!(marked.has_mark_bit());
347                assert!(!cleared.has_mark_bit());
348                // Clearing should recover the original pointer
349                assert_eq!(gc_ptr.as_ptr(), cleared.as_ptr());
350            }
351        }
352
353        #[cfg(target_arch = "aarch64")]
354        {
355            // ARM TBI: mark bit always works
356            assert!(marked.has_mark_bit());
357            assert!(!cleared.has_mark_bit());
358            // The raw address portion should be preserved
359            assert_eq!(gc_ptr.raw_ptr(), marked.raw_ptr());
360        }
361
362        // Generic fallback check for other architectures
363        #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
364        {
365            assert!(!marked.has_mark_bit());
366            assert!(!cleared.has_mark_bit());
367            assert_eq!(gc_ptr.as_ptr(), marked.as_ptr());
368        }
369    }
370
371    #[test]
372    fn test_mark_bit_idempotent() {
373        let mut buf = [0u8; 16];
374        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
375        unsafe { header_ptr.write(GcHeader::new(0, 8)) };
376        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
377        unsafe { data_ptr.write(0) };
378
379        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
380        let marked_once = gc_ptr.with_mark_bit();
381        let marked_twice = marked_once.with_mark_bit();
382        // Applying mark bit twice should yield the same pointer
383        assert_eq!(marked_once.as_ptr(), marked_twice.as_ptr());
384    }
385
386    #[test]
387    fn test_is_marked_uses_header_fallback() {
388        // is_marked() should return true when the GcHeader color is not White,
389        // even when the inline mark bit is not set (software fallback).
390        let mut buf = [0u8; 16];
391        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
392        unsafe { header_ptr.write(GcHeader::new(0, 8)) };
393        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
394        unsafe { data_ptr.write(42) };
395
396        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
397
398        // Initially white — not marked
399        assert!(!gc_ptr.is_marked());
400
401        // Set header to Gray — is_marked should return true via fallback
402        let header = unsafe { gc_ptr.header_mut() };
403        header.set_color(GcColor::Gray);
404        assert!(gc_ptr.is_marked());
405
406        // Set header to Black — still marked
407        header.set_color(GcColor::Black);
408        assert!(gc_ptr.is_marked());
409
410        // Reset to White — no longer marked
411        header.set_color(GcColor::White);
412        assert!(!gc_ptr.is_marked());
413    }
414
415    #[test]
416    fn test_raw_ptr_strips_metadata() {
417        let mut buf = [0u8; 16];
418        let header_ptr = buf.as_mut_ptr() as *mut GcHeader;
419        unsafe { header_ptr.write(GcHeader::new(0, 8)) };
420        let data_ptr = unsafe { buf.as_mut_ptr().add(8) } as *mut u64;
421        unsafe { data_ptr.write(0) };
422
423        let gc_ptr = unsafe { GcPtr::<u64>::from_raw(data_ptr) };
424        let marked = gc_ptr.with_mark_bit();
425
426        // raw_ptr() should strip any metadata bits and return the clean address.
427        // On software fallback, with_mark_bit is a no-op so raw_ptr == as_ptr.
428        let raw = marked.raw_ptr();
429        // The raw pointer's lower 48 bits must match the original data_ptr
430        assert_eq!(
431            raw as usize & 0x0000_FFFF_FFFF_FFFF,
432            data_ptr as usize & 0x0000_FFFF_FFFF_FFFF,
433        );
434    }
435}