Skip to main content

tidepool_codegen/gc/
frame_walker.rs

1use crate::stack_map::StackMapRegistry;
2
3/// A collected GC root: the address on the stack where a heap pointer lives.
4#[derive(Debug, Clone, Copy)]
5pub struct StackRoot {
6    /// Address on the stack containing the heap pointer.
7    pub stack_slot_addr: *mut u64,
8    /// Current value of the heap pointer.
9    pub heap_ptr: *mut u8,
10}
11
12/// Walk JIT frames starting from the given RBP, collecting all GC roots.
13///
14/// # Safety
15/// - `start_rbp` must be a valid frame pointer from within a JIT call chain.
16/// - `stack_maps` must contain entries for all JIT functions in the call chain.
17#[cfg(target_arch = "x86_64")]
18pub unsafe fn walk_frames(
19    start_rbp: usize,
20    stack_maps: &StackMapRegistry,
21    start_rsp: usize,
22) -> Vec<StackRoot> {
23    let mut roots = Vec::new();
24    let mut rbp = start_rbp;
25    
26    // First, try to find the first JIT return address by searching up from RSP.
27    // This handles cases where gc_trigger doesn't have a frame pointer.
28    let mut current_return_addr = None;
29    let mut search_ptr = start_rsp;
30    // We search up to RBP + 16 (where the JIT return addr would be if gc_trigger has no frame).
31    while search_ptr < start_rbp + 16 {
32        let val = *(search_ptr as *const usize);
33        if stack_maps.contains_address(val) {
34            current_return_addr = Some(val);
35            // If we found a JIT return address, the RBP for this frame is the current RBP
36            // if gc_trigger has no frame, or it's the saved RBP if it does.
37            // Actually, if we found a JIT return address at search_ptr, 
38            // then search_ptr + 8 is the SP at the safepoint!
39            break;
40        }
41        search_ptr += 8;
42    }
43
44    loop {
45        if rbp == 0 {
46            break;
47        }
48
49        // Determine return address for this frame
50        let return_addr = if let Some(addr) = current_return_addr.take() {
51            addr
52        } else {
53            *((rbp + 8) as *const usize)
54        };
55        
56        // Check if this return address is in JIT code
57        if !stack_maps.contains_address(return_addr) {
58            if !roots.is_empty() {
59                // We were in JIT territory and now we left it. Stop.
60                break;
61            } else {
62                // We haven't hit JIT territory yet. Skip this frame.
63                let next_rbp = *(rbp as *const usize);
64                if next_rbp == 0 || next_rbp == rbp || next_rbp < rbp {
65                    break;
66                }
67                rbp = next_rbp;
68                continue;
69            }
70        }
71
72        // Look up stack map for this return address
73        if let Some(info) = stack_maps.lookup(return_addr) {
74            // Compute SP at safepoint.
75            // Cranelift stack map offsets are SP-relative at the safepoint.
76            // The return address we found is the one pushed by the 'call' in JIT code.
77            // The SP just before that 'call' was (addr_of_return_addr + 8).
78            
79            // We need to find where this return_addr was on the stack.
80            //
81            // If this is the first frame we found, and it was found via the initial RSP search
82            // (proxied by `search_ptr < start_rbp + 16`), we use that search address.
83            // Otherwise, for any subsequent JIT frames, the return address is at [rbp + 8].
84            let addr_of_return_addr = if roots.is_empty() && search_ptr < start_rbp + 16 {
85                search_ptr
86            } else {
87                rbp + 8
88            };
89            
90            let sp_at_safepoint = addr_of_return_addr + 8;
91
92            for &offset in &info.offsets {
93                let root_addr = (sp_at_safepoint + offset as usize) as *mut u64;
94                let heap_ptr = *root_addr as *mut u8;
95                roots.push(StackRoot {
96                    stack_slot_addr: root_addr,
97                    heap_ptr,
98                });
99            }
100        }
101
102        // Walk to next frame: *(rbp) is the saved caller RBP
103        let next_rbp = *(rbp as *const usize);
104        
105        // Basic sanity checks to prevent infinite loops or jumping to null
106        if next_rbp == 0 || next_rbp == rbp || next_rbp < rbp {
107            break;
108        }
109        rbp = next_rbp;
110    }
111
112    roots
113}
114
115/// Rewrite forwarding pointers in stack slots after GC.
116///
117/// For each root, if the heap object has been moved (forwarding pointer),
118/// update the stack slot to point to the new location.
119///
120/// # Safety
121/// All roots must still be valid stack addresses.
122pub unsafe fn rewrite_roots(roots: &[StackRoot], forwarding_map: &dyn Fn(*mut u8) -> *mut u8) {
123    for root in roots {
124        let new_ptr = forwarding_map(root.heap_ptr);
125        if new_ptr != root.heap_ptr {
126            *root.stack_slot_addr = new_ptr as u64;
127        }
128    }
129}