rune/core/gc/
context.rs

1use super::GcState;
2use super::Trace;
3use crate::core::object::GcString;
4use crate::core::object::LispHashTable;
5use crate::core::object::{Gc, IntoObject, Object, UninternedSymbolMap, WithLifetime};
6use bumpalo::collections::Vec as GcVec;
7use std::cell::{Cell, RefCell};
8use std::fmt::Debug;
9use std::ops::Deref;
10use std::sync::atomic::AtomicBool;
11
12/// A global store of all gc roots. This struct should be passed to the [Context]
13/// when it is created.
14#[derive(Default, Debug)]
15pub(crate) struct RootSet {
16    pub(super) roots: RefCell<Vec<*const dyn Trace>>,
17}
18
19/// Thread-safe version of RootSet that uses Mutex instead of RefCell.
20/// This is specifically designed for multi-threaded scenarios like ChannelManager
21/// where roots need to be managed across thread boundaries.
22///
23/// Note: Send/Sync safety is guaranteed by HeapRoot, which ensures the raw pointers
24/// only point to data it owns in HeapRoot::data.
25#[derive(Debug)]
26pub(crate) struct ThreadSafeRootSet {
27    pub(super) roots: std::sync::Mutex<Vec<*const dyn Trace>>,
28}
29
30impl Default for ThreadSafeRootSet {
31    fn default() -> Self {
32        Self { roots: std::sync::Mutex::new(Vec::new()) }
33    }
34}
35
36#[expect(dead_code)]
37// These types are only stored here so they can be dropped
38pub(in crate::core) enum DropStackElem {
39    String(String),
40    ByteString(Vec<u8>),
41    Vec(Vec<Object<'static>>),
42}
43
44/// A block of allocations. This type should be owned by [Context] and not used
45/// directly.
46#[derive(Default)]
47pub(crate) struct Block<const CONST: bool> {
48    pub(in crate::core) objects: bumpalo::Bump,
49    // Allocations that will be dropped when the objects are moved. At that time
50    // the allocation will get copied into the GC heap. This let's us avoid an
51    // extra copy of memory when a vector is first made an object. The
52    // allocation can continue to live outside of the GC heap until we copy the
53    // object.
54    pub(in crate::core) drop_stack: RefCell<Vec<DropStackElem>>,
55    // We don't yet have a hashmap that supports allocators, so we need to keep
56    // track of the memory and free it only after the table is garbage
57    // collected. Kind of a hack.
58    pub(in crate::core) lisp_hashtables: RefCell<Vec<*const LispHashTable>>,
59    pub(in crate::core) uninterned_symbol_map: UninternedSymbolMap,
60}
61
62unsafe impl<const C: bool> Send for Block<C> {}
63
64/// Owns all allocations and creates objects. All objects have
65/// a lifetime tied to the borrow of their `Context`. When the
66/// `Context` goes out of scope, no objects should be accessible.
67pub(crate) struct Context<'rt> {
68    pub(crate) block: Block<false>,
69    root_set: &'rt RootSet,
70    next_limit: usize,
71}
72
73impl Drop for Context<'_> {
74    fn drop(&mut self) {
75        self.garbage_collect(true);
76        if self.block.objects.allocated_bytes() == 0 {
77            return;
78        }
79        if std::thread::panicking() {
80            eprintln!("Error: Context was dropped while still holding data");
81        } else {
82            panic!("Error: Context was dropped while still holding data");
83        }
84    }
85}
86
87thread_local! {
88    /// Ensure there is only one context per thread.
89    static SINGLETON_CHECK: Cell<bool> = const { Cell::new(false) };
90}
91
92/// Ensure there is only one global context.
93static GLOBAL_CHECK: AtomicBool = AtomicBool::new(false);
94
95impl Block<true> {
96    pub(crate) fn new_global() -> Self {
97        use std::sync::atomic::Ordering::SeqCst as Ord;
98        assert!(GLOBAL_CHECK.compare_exchange(false, true, Ord, Ord).is_ok());
99        Self::default()
100    }
101}
102
103impl Block<false> {
104    pub(crate) fn new_local() -> Self {
105        Self::assert_unique();
106        Self::default()
107    }
108
109    pub(crate) fn new_local_unchecked() -> Self {
110        Self::default()
111    }
112
113    pub(crate) fn assert_unique() {
114        SINGLETON_CHECK.with(|x| {
115            assert!(!x.get(), "There was already and active context when this context was created");
116            x.set(true);
117        });
118    }
119}
120
121impl<const CONST: bool> Block<CONST> {
122    pub(crate) fn add<'ob, T, Tx>(&'ob self, obj: T) -> Object<'ob>
123    where
124        T: IntoObject<Out<'ob> = Tx>,
125        Gc<Tx>: Into<Object<'ob>>,
126    {
127        obj.into_obj(self).into()
128    }
129
130    pub(crate) fn add_as<'ob, T, Tx, V>(&'ob self, obj: T) -> Gc<V>
131    where
132        T: IntoObject<Out<'ob> = Tx>,
133        Gc<Tx>: Into<Gc<V>>,
134    {
135        obj.into_obj(self).into()
136    }
137
138    /// Create a new String whose backing storage is already part of the GC
139    /// heap. Does not require dropping when moved during garbage collection
140    /// (unlike std::string).
141    pub(crate) fn string_with_capacity(&self, cap: usize) -> GcString<'_> {
142        GcString::with_capacity_in(cap, &self.objects)
143    }
144
145    /// Create a new Vec whose backing storage is already part of the GC
146    /// heap. Does not require dropping when moved during garbage collection
147    /// (unlike std::vec).
148    pub(crate) fn vec_new(&self) -> GcVec<'_, Object<'_>> {
149        GcVec::new_in(&self.objects)
150    }
151
152    pub(crate) fn vec_with_capacity(&self, cap: usize) -> GcVec<'_, Object<'_>> {
153        GcVec::with_capacity_in(cap, &self.objects)
154    }
155}
156
157impl<'ob, 'rt> Context<'rt> {
158    pub(crate) const MIN_GC_BYTES: usize = 2000;
159    pub(crate) const GC_GROWTH_FACTOR: usize = 12; // divide by 10
160    pub(crate) fn new(roots: &'rt RootSet) -> Self {
161        Self { block: Block::new_local(), root_set: roots, next_limit: Self::MIN_GC_BYTES }
162    }
163
164    pub(crate) fn from_block(block: Block<false>, roots: &'rt RootSet) -> Self {
165        Block::assert_unique();
166        Context { block, root_set: roots, next_limit: Self::MIN_GC_BYTES }
167    }
168
169    pub(crate) fn bind<T>(&'ob self, obj: T) -> <T as WithLifetime<'ob>>::Out
170    where
171        T: WithLifetime<'ob>,
172    {
173        unsafe { obj.with_lifetime() }
174    }
175
176    pub(crate) fn get_root_set(&'ob self) -> &'rt RootSet {
177        self.root_set
178    }
179
180    pub(crate) fn garbage_collect(&mut self, force: bool) {
181        let bytes = self.block.objects.allocated_bytes();
182        if cfg!(not(test)) && !force && bytes < self.next_limit {
183            return;
184        }
185
186        let mut state = GcState::new();
187        for x in self.root_set.roots.borrow().iter() {
188            // SAFETY: The contract of root structs will ensure that it removes
189            // itself from this list before it drops.
190            unsafe {
191                (**x).trace(&mut state);
192            }
193        }
194
195        state.trace_stack();
196
197        self.next_limit = (state.to_space.allocated_bytes() * Self::GC_GROWTH_FACTOR) / 10;
198        self.block.drop_stack.borrow_mut().clear();
199        // Find all hashtables that have not been moved (i.e. They are no longer
200        // accessible) and drop them. Otherwise, update the object pointer.
201        self.block.lisp_hashtables.borrow_mut().retain_mut(|ptr| {
202            let table = unsafe { &**ptr };
203            if let Some(fwd) = table.forwarding_ptr() {
204                *ptr = fwd.as_ptr().cast::<LispHashTable>();
205                true
206            } else {
207                unsafe { std::ptr::drop_in_place(*ptr as *mut LispHashTable) };
208                false
209            }
210        });
211
212        self.block.objects = state.to_space;
213    }
214}
215
216/// Perform garbage collection on a block with a root set without requiring a Context.
217/// This is useful for scenarios like ChannelManager where we need to GC a shared block
218/// from any thread without conflicting with thread-local Context singleton checks.
219///
220/// # Safety
221/// Caller must ensure:
222/// 1. Exclusive access to the block (no concurrent reads/writes)
223/// 2. The root_set accurately represents all live references to objects in the block
224/// 3. The block's drop_stack and lisp_hashtables are properly maintained
225/// 4. No active Context exists that references this block or root_set
226pub(crate) unsafe fn collect_garbage_raw(
227    block: &mut Block<false>,
228    root_set: &ThreadSafeRootSet,
229    force: bool,
230    next_limit: &mut usize,
231) {
232    let bytes = block.objects.allocated_bytes();
233    if cfg!(not(test)) && !force && bytes < *next_limit {
234        return;
235    }
236
237    let mut state = GcState::new();
238    for x in root_set.roots.lock().unwrap().iter() {
239        // SAFETY: The contract of root structs will ensure that it removes
240        // itself from this list before it drops.
241        unsafe {
242            (**x).trace(&mut state);
243        }
244    }
245
246    state.trace_stack();
247
248    *next_limit = (state.to_space.allocated_bytes() * Context::GC_GROWTH_FACTOR) / 10;
249    block.drop_stack.borrow_mut().clear();
250    // Find all hashtables that have not been moved (i.e. They are no longer
251    // accessible) and drop them. Otherwise, update the object pointer.
252    block.lisp_hashtables.borrow_mut().retain_mut(|ptr| {
253        let table = unsafe { &**ptr };
254        if let Some(fwd) = table.forwarding_ptr() {
255            *ptr = fwd.as_ptr().cast::<LispHashTable>();
256            true
257        } else {
258            unsafe { std::ptr::drop_in_place(*ptr as *mut LispHashTable) };
259            false
260        }
261    });
262
263    block.objects = state.to_space;
264}
265
266impl Deref for Context<'_> {
267    type Target = Block<false>;
268
269    fn deref(&self) -> &Self::Target {
270        &self.block
271    }
272}
273
274impl AsRef<Block<false>> for Context<'_> {
275    fn as_ref(&self) -> &Block<false> {
276        &self.block
277    }
278}
279
280impl<const CONST: bool> Drop for Block<CONST> {
281    // Only one block can exist in a thread at a time. This part of that
282    // contract.
283    fn drop(&mut self) {
284        SINGLETON_CHECK.with(|s| {
285            assert!(s.get(), "Context singleton check was overwritten");
286            s.set(false);
287        });
288    }
289}
290
291#[cfg(test)]
292mod test {
293    use rune_core::macros::{list, rebind, root};
294
295    use crate::core::{
296        cons::Cons,
297        object::{HashTable, ObjectType, Symbol},
298    };
299
300    use super::*;
301    fn bind_to_mut<'ob>(cx: &'ob mut Context) -> Object<'ob> {
302        cx.add("invariant")
303    }
304
305    #[test]
306    fn test_reborrow() {
307        let roots = &RootSet::default();
308        let cx = &mut Context::new(roots);
309        let obj = rebind!(bind_to_mut(cx));
310        _ = "foo".into_obj(cx);
311        assert_eq!(obj, "invariant");
312    }
313
314    #[test]
315    fn test_garbage_collect() {
316        let roots = &RootSet::default();
317        let cx = &mut Context::new(roots);
318        root!(vec, new(Vec), cx);
319        cx.garbage_collect(true);
320        let cons = list!["foo", 1, false, "end"; cx];
321        vec.push(cons);
322        cx.garbage_collect(true);
323    }
324
325    #[test]
326    fn test_move_values() {
327        let roots = &RootSet::default();
328        let cx = &mut Context::new(roots);
329        let int = cx.add(1);
330        let float = cx.add(1.5);
331        let cons: Object = Cons::new(int, float, cx).into();
332        let string = cx.add("string");
333        let symbol = cx.add(Symbol::new_uninterned("sym", cx));
334        println!("sym: {:?}", symbol.into_raw());
335        let mut table = HashTable::default();
336        table.insert(symbol, string);
337        let _ = table.get(&symbol).unwrap();
338        root!(symbol, cx);
339        let table = cx.add(table);
340        let vec = vec![cons, table];
341        let vec = cx.add(vec);
342        root!(vec, cx);
343        cx.garbage_collect(true);
344        let vec = vec.bind(cx);
345        let ObjectType::Vec(vec) = vec.untag() else { unreachable!() };
346        let ObjectType::Cons(cons) = vec[0].get().untag() else { unreachable!() };
347        let ObjectType::HashTable(table) = vec[1].get().untag() else { unreachable!() };
348        let key = symbol.bind(cx);
349        println!("key: {:?}", key.into_raw());
350        let val = table.get(symbol.bind(cx)).unwrap();
351        let ObjectType::String(string) = val.untag() else { unreachable!() };
352        let ObjectType::Int(int) = cons.car().untag() else { unreachable!() };
353        let ObjectType::Float(float) = cons.cdr().untag() else { unreachable!() };
354        assert_eq!(string, "string");
355        assert_eq!(**float, 1.5);
356        assert_eq!(int, 1);
357    }
358}