Skip to main content

shape_gc/
relocator.rs

1//! Concurrent relocation with forwarding table.
2//!
3//! After marking, the GC can relocate live objects to compact memory:
4//! 1. Walk live (black) objects in a source region.
5//! 2. Copy each to a target region (bump allocator).
6//! 3. Insert old→new mapping in the forwarding table.
7//! 4. Protect old pages (PROT_NONE) to trap stale accesses.
8
9use crate::header::{GcColor, GcHeader};
10use crate::region::Region;
11use std::collections::HashMap;
12
13/// Maps old object addresses to their new (relocated) addresses.
14pub struct ForwardingTable {
15    table: HashMap<usize, *mut u8>,
16}
17
18// Safety: ForwardingTable is used during stop-the-world or with trap handler synchronization.
19unsafe impl Send for ForwardingTable {}
20unsafe impl Sync for ForwardingTable {}
21
22impl ForwardingTable {
23    pub fn new() -> Self {
24        Self {
25            table: HashMap::with_capacity(4096),
26        }
27    }
28
29    /// Insert a forwarding entry: old_ptr → new_ptr.
30    pub fn insert(&mut self, old: *mut u8, new: *mut u8) {
31        self.table.insert(old as usize, new);
32    }
33
34    /// Look up the new address for an old pointer.
35    pub fn lookup(&self, old: *const u8) -> Option<*mut u8> {
36        self.table.get(&(old as usize)).copied()
37    }
38
39    /// Number of forwarding entries.
40    pub fn len(&self) -> usize {
41        self.table.len()
42    }
43
44    /// Check if the table is empty.
45    pub fn is_empty(&self) -> bool {
46        self.table.is_empty()
47    }
48
49    /// Clear all entries.
50    pub fn clear(&mut self) {
51        self.table.clear();
52    }
53
54    /// Iterate over all forwarding entries.
55    pub fn iter(&self) -> impl Iterator<Item = (*mut u8, *mut u8)> + '_ {
56        self.table.iter().map(|(&old, &new)| (old as *mut u8, new))
57    }
58}
59
60impl Default for ForwardingTable {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66/// The `Relocator` orchestrates compaction by owning a `ForwardingTable` and
67/// providing methods to copy live objects between regions, install forwarding
68/// entries, and resolve stale pointers.
69pub struct Relocator {
70    /// Old→new address mapping populated during compaction.
71    forwarding_table: ForwardingTable,
72}
73
74impl Relocator {
75    /// Create a new relocator with an empty forwarding table.
76    pub fn new() -> Self {
77        Self {
78            forwarding_table: ForwardingTable::new(),
79        }
80    }
81
82    /// Install a forwarding entry: `old_addr` now lives at `new_addr`.
83    pub fn install_forwarding(&mut self, old_addr: *mut u8, new_addr: *mut u8) {
84        self.forwarding_table.insert(old_addr, new_addr);
85    }
86
87    /// Resolve a (possibly stale) pointer through the forwarding table.
88    ///
89    /// If `ptr` has a forwarding entry, returns the new address. Otherwise
90    /// returns `ptr` unchanged.
91    pub fn resolve(&self, ptr: *mut u8) -> *mut u8 {
92        self.forwarding_table
93            .lookup(ptr as *const u8)
94            .unwrap_or(ptr)
95    }
96
97    /// Compact a source region by copying all live (Black) objects into `target`.
98    ///
99    /// Each copied object is:
100    /// 1. Allocated in `target` via bump allocation.
101    /// 2. Memory-copied (header + object data).
102    /// 3. Its header reset to White (ready for the next marking cycle).
103    /// 4. Recorded in the forwarding table.
104    ///
105    /// Returns the number of objects moved.
106    pub fn compact_region(&mut self, source: &Region, target: &mut Region) -> usize {
107        let header_size = std::mem::size_of::<GcHeader>();
108        let mut moved = 0;
109
110        source.for_each_object(|header, old_obj_ptr| {
111            // Only compact live (Black) objects
112            if header.color() != GcColor::Black {
113                return;
114            }
115
116            let obj_size = header.size as usize;
117            let layout = std::alloc::Layout::from_size_align(obj_size, 8).unwrap();
118
119            // Allocate in target region
120            if let Some(new_obj_ptr) = target.try_alloc(layout) {
121                // Copy object data
122                unsafe {
123                    std::ptr::copy_nonoverlapping(old_obj_ptr, new_obj_ptr, obj_size);
124                }
125
126                // Update the header in the target region
127                let new_header =
128                    unsafe { &mut *((new_obj_ptr as *mut u8).sub(header_size) as *mut GcHeader) };
129                *new_header = *header;
130                new_header.set_color(GcColor::White); // Reset for next cycle
131
132                // Mark the source header as forwarded
133                let old_header =
134                    unsafe { &mut *((old_obj_ptr as *mut u8).sub(header_size) as *mut GcHeader) };
135                old_header.set_forwarded(true);
136
137                // Record the forwarding entry
138                self.forwarding_table.insert(old_obj_ptr, new_obj_ptr);
139                moved += 1;
140            }
141        });
142
143        moved
144    }
145
146    /// Get a reference to the underlying forwarding table.
147    pub fn forwarding_table(&self) -> &ForwardingTable {
148        &self.forwarding_table
149    }
150
151    /// Get a mutable reference to the underlying forwarding table.
152    pub fn forwarding_table_mut(&mut self) -> &mut ForwardingTable {
153        &mut self.forwarding_table
154    }
155
156    /// Clear the forwarding table for reuse.
157    pub fn reset(&mut self) {
158        self.forwarding_table.clear();
159    }
160}
161
162impl Default for Relocator {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168/// Relocate all live objects from `source` into `target`.
169///
170/// Returns the forwarding table with old→new mappings.
171///
172/// After calling this, the caller should:
173/// 1. Run fixup on all live objects and roots to update stale pointers.
174/// 2. Protect the source region (or return it to the free pool).
175pub fn relocate_region(source: &Region, target: &mut Region) -> ForwardingTable {
176    let mut relocator = Relocator::new();
177    relocator.compact_region(source, target);
178    // Move the forwarding table out of the relocator
179    let mut ft = ForwardingTable::new();
180    std::mem::swap(&mut ft, relocator.forwarding_table_mut());
181    ft
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_forwarding_table_insert_lookup() {
190        let mut ft = ForwardingTable::new();
191        let old = 0x1000 as *mut u8;
192        let new = 0x2000 as *mut u8;
193        ft.insert(old, new);
194
195        assert_eq!(ft.lookup(old), Some(new));
196        assert_eq!(ft.lookup(0x3000 as *const u8), None);
197        assert_eq!(ft.len(), 1);
198    }
199
200    #[test]
201    fn test_forwarding_table_overwrite() {
202        let mut ft = ForwardingTable::new();
203        let old = 0x1000 as *mut u8;
204        ft.insert(old, 0x2000 as *mut u8);
205        ft.insert(old, 0x3000 as *mut u8);
206        // Last insertion wins
207        assert_eq!(ft.lookup(old), Some(0x3000 as *mut u8));
208        assert_eq!(ft.len(), 1);
209    }
210
211    #[test]
212    fn test_forwarding_table_clear() {
213        let mut ft = ForwardingTable::new();
214        ft.insert(0x1000 as *mut u8, 0x2000 as *mut u8);
215        ft.insert(0x3000 as *mut u8, 0x4000 as *mut u8);
216        assert_eq!(ft.len(), 2);
217
218        ft.clear();
219        assert!(ft.is_empty());
220        assert_eq!(ft.len(), 0);
221        assert_eq!(ft.lookup(0x1000 as *const u8), None);
222    }
223
224    #[test]
225    fn test_forwarding_table_iter() {
226        let mut ft = ForwardingTable::new();
227        ft.insert(0x1000 as *mut u8, 0x2000 as *mut u8);
228        ft.insert(0x3000 as *mut u8, 0x4000 as *mut u8);
229
230        let entries: Vec<_> = ft.iter().collect();
231        assert_eq!(entries.len(), 2);
232    }
233
234    // ── Relocator tests ─────────────────────────────────────────────
235
236    #[test]
237    fn test_relocator_install_and_resolve() {
238        let mut relocator = Relocator::new();
239        let old = 0x1000 as *mut u8;
240        let new = 0x2000 as *mut u8;
241
242        // Before install, resolve returns the original pointer
243        assert_eq!(relocator.resolve(old), old);
244
245        relocator.install_forwarding(old, new);
246
247        // After install, resolve returns the new pointer
248        assert_eq!(relocator.resolve(old), new);
249
250        // Unknown pointer resolves to itself
251        let unknown = 0x9000 as *mut u8;
252        assert_eq!(relocator.resolve(unknown), unknown);
253    }
254
255    #[test]
256    fn test_relocator_reset() {
257        let mut relocator = Relocator::new();
258        relocator.install_forwarding(0x1000 as *mut u8, 0x2000 as *mut u8);
259        assert_eq!(relocator.forwarding_table().len(), 1);
260
261        relocator.reset();
262        assert!(relocator.forwarding_table().is_empty());
263    }
264
265    #[test]
266    fn test_relocator_compact_region() {
267        // Allocate a source region with some objects
268        let mut source = Region::new();
269        let layout = std::alloc::Layout::from_size_align(64, 8).unwrap();
270
271        // Allocate 3 objects
272        let obj1 = source.try_alloc(layout).unwrap();
273        let obj2 = source.try_alloc(layout).unwrap();
274        let obj3 = source.try_alloc(layout).unwrap();
275
276        // Write recognizable patterns into the objects
277        unsafe {
278            std::ptr::write_bytes(obj1, 0xAA, 64);
279            std::ptr::write_bytes(obj2, 0xBB, 64);
280            std::ptr::write_bytes(obj3, 0xCC, 64);
281        }
282
283        // Mark obj1 and obj3 as live (Black), leave obj2 as White (dead)
284        let header_size = std::mem::size_of::<GcHeader>();
285        let h1 = unsafe { &mut *((obj1 as *mut u8).sub(header_size) as *mut GcHeader) };
286        let h3 = unsafe { &mut *((obj3 as *mut u8).sub(header_size) as *mut GcHeader) };
287        h1.set_color(GcColor::Black);
288        h3.set_color(GcColor::Black);
289        // obj2's header stays White
290
291        // Create a target region and compact
292        let mut target = Region::new();
293        let mut relocator = Relocator::new();
294        let moved = relocator.compact_region(&source, &mut target);
295
296        // Should have moved exactly 2 objects (obj1 and obj3)
297        assert_eq!(moved, 2);
298        assert_eq!(relocator.forwarding_table().len(), 2);
299
300        // Verify forwarding entries exist for obj1 and obj3
301        let new_obj1 = relocator.resolve(obj1);
302        let new_obj3 = relocator.resolve(obj3);
303        assert_ne!(new_obj1, obj1); // Should have moved
304        assert_ne!(new_obj3, obj3);
305
306        // obj2 should NOT have a forwarding entry
307        assert_eq!(relocator.resolve(obj2), obj2);
308
309        // Verify the data was copied correctly
310        unsafe {
311            assert_eq!(*new_obj1, 0xAA);
312            assert_eq!(*new_obj3, 0xCC);
313        }
314
315        // Verify the target headers were reset to White
316        let new_h1 = unsafe { &*((new_obj1 as *mut u8).sub(header_size) as *const GcHeader) };
317        let new_h3 = unsafe { &*((new_obj3 as *mut u8).sub(header_size) as *const GcHeader) };
318        assert_eq!(new_h1.color(), GcColor::White);
319        assert_eq!(new_h3.color(), GcColor::White);
320
321        // Verify the source headers were marked as forwarded
322        assert!(h1.is_forwarded());
323        assert!(h3.is_forwarded());
324    }
325
326    #[test]
327    fn test_relocator_compact_region_empty_source() {
328        let source = Region::new();
329        let mut target = Region::new();
330        let mut relocator = Relocator::new();
331
332        let moved = relocator.compact_region(&source, &mut target);
333        assert_eq!(moved, 0);
334        assert!(relocator.forwarding_table().is_empty());
335    }
336
337    #[test]
338    fn test_relocator_compact_region_all_dead() {
339        let mut source = Region::new();
340        let layout = std::alloc::Layout::from_size_align(32, 8).unwrap();
341
342        // Allocate objects but leave them all White (dead)
343        let _obj1 = source.try_alloc(layout).unwrap();
344        let _obj2 = source.try_alloc(layout).unwrap();
345
346        let mut target = Region::new();
347        let mut relocator = Relocator::new();
348        let moved = relocator.compact_region(&source, &mut target);
349
350        assert_eq!(moved, 0);
351        assert!(relocator.forwarding_table().is_empty());
352    }
353
354    #[test]
355    fn test_relocate_region_compatibility() {
356        // Test that the top-level relocate_region function still works
357        let mut source = Region::new();
358        let layout = std::alloc::Layout::from_size_align(32, 8).unwrap();
359        let obj = source.try_alloc(layout).unwrap();
360
361        // Mark as live
362        let header_size = std::mem::size_of::<GcHeader>();
363        let h = unsafe { &mut *((obj as *mut u8).sub(header_size) as *mut GcHeader) };
364        h.set_color(GcColor::Black);
365
366        let mut target = Region::new();
367        let ft = relocate_region(&source, &mut target);
368        assert_eq!(ft.len(), 1);
369        assert!(ft.lookup(obj).is_some());
370    }
371}