Skip to main content

lua_types/
gc.rs

1//! `GcRef<T>` — the GC-managed reference handle.
2//!
3//! Newtype around `lua_gc::Gc<T>` — Copy under the hood, tracks allocation
4//! in the active `Heap` (via `lua_gc::with_current_heap(...)`). An earlier
5//! revision was a thin `Rc<T>` newtype instead; the surface stayed stable
6//! across the swap (`new`, `ptr_eq`, `identity`, `strong_count`,
7//! `weak_count`, `downgrade`), and existing code touching `gc.0` continues
8//! to work — `.0` is now `Gc<T>` instead of `Rc<T>`.
9//!
10//! # Weak refs
11//!
12//! Heap-tracked `GcWeak<T>` handles remember the heap active when they were
13//! created plus the target's heap allocation token. They upgrade only while
14//! that identity/token pair remains live. Handles to legacy uncollected boxes
15//! still upgrade forever, matching their process-lifetime allocation model.
16
17use lua_gc::{Gc, HeapRef, Marker, Trace};
18
19/// A GC-managed pointer to a Lua collectable object. Newtype over
20/// `lua_gc::Gc<T>` so callers preserve `gc.0`-shape access while the
21/// backend swaps under them.
22#[derive(Debug)]
23pub struct GcRef<T: Trace + 'static>(pub Gc<T>);
24
25impl<T: Trace + 'static> GcRef<T> {
26    /// Allocate a new GC-tracked value on the active heap: joins the allgc
27    /// chain under a `HeapGuard` (set by `state.run()` / `pcall_k` /
28    /// `with_state`), or the heap's bootstrap list inside a bootstrap
29    /// window.
30    ///
31    /// Panics if no heap is active. There is no fallback: the old detached
32    /// arm allocated a box no heap ever freed (issue #249's leak class), and
33    /// since #253 moved host-side `LuaError` messages to owned bytes, no
34    /// legitimate guard-less allocation path remains — a panic here is
35    /// always a missing guard on the entry path, and the message says so.
36    pub fn new(value: T) -> Self {
37        let gc = lua_gc::with_current_heap(|heap| match heap {
38            Some(heap) => heap.allocate(value),
39            None => panic!(
40                "GcRef::new::<{}> with no active HeapGuard — a detached allocation \
41                 would never be freed (issue #249 class); push a HeapGuard or \
42                 bootstrap window on the entry path",
43                std::any::type_name::<T>()
44            ),
45        });
46        GcRef(gc)
47    }
48
49    /// Cycle-aware trace dispatch via `Marker::try_visit`'s identity
50    /// short-circuit, rather than the in-header color flag that
51    /// `Trace for GcRef<T>` (in `trace_impls.rs`) uses directly via
52    /// `m.mark(self.0)`. Nothing in this workspace currently calls
53    /// `trace_obj`.
54    pub fn trace_obj(&self, m: &mut Marker) {
55        if m.try_visit(self.identity()) {
56            (**self).trace(m);
57        }
58    }
59}
60
61impl<T: Trace + 'static> GcRef<T> {
62    /// Two `GcRef`s are identity-equal iff they point at the same box.
63    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
64        Gc::ptr_eq(a.0, b.0)
65    }
66
67    /// Identity as a usize — used as a HashMap key for "same object" tests.
68    pub fn identity(&self) -> usize {
69        self.0.identity()
70    }
71
72    /// Number of strong references. `GcRef` is not reference-counted, so a live
73    /// handle reports one owning GC reachability handle.
74    pub fn strong_count(&self) -> usize {
75        1
76    }
77
78    /// Number of weak references. Weak handles are not counted.
79    pub fn weak_count(&self) -> usize {
80        0
81    }
82
83    /// Get a weak handle. If this allocation belongs to the currently-active
84    /// heap, the weak handle will stop upgrading once sweep removes that exact
85    /// heap allocation.
86    ///
87    /// Downgrading a *heap-owned* box with no active `HeapGuard` is a
88    /// latent use-after-free: the resulting handle has no heap identity to
89    /// check, so it keeps upgrading after sweep frees the target. That case
90    /// panics unconditionally in every build; detached (process-lifetime)
91    /// targets keep the legacy always-upgrades behavior.
92    ///
93    /// A `GcRef` obtained before its heap closed (`Heap::drop_all` /
94    /// `close`) is dangling afterwards, and calling this on it is
95    /// use-after-free — the same contract as after `Heap::drop`. (Quarantine
96    /// mode detects use-after-*sweep* while the heap is alive; teardown
97    /// frees even quarantined boxes for real, so post-close dereferences are
98    /// plain UB with no tripwire.) When the *closed* heap is the active
99    /// guard, the closed-heap token refusal makes the resulting weak handle
100    /// permanently dead; with no guard or a different heap active, the
101    /// guard-mismatch cases above apply unchanged. A `Gc` box carries no
102    /// owner identity of its own to validate against — fixing that is the
103    /// heap-ownership redesign tracked as the #252 follow-up, not a #260
104    /// teardown property.
105    pub fn downgrade(&self) -> GcWeak<T> {
106        let identity = self.identity();
107        let tracked = lua_gc::with_current_heap(|heap| {
108            heap.map(|heap| {
109                let token = heap.register_allocation_token(identity);
110                (HeapRef::from_heap(heap), token)
111            })
112        });
113        if tracked.is_none() && self.0.is_heap_owned() {
114            panic!(
115                "GcRef::downgrade::<{}> on a heap-owned box with no \
116                 active HeapGuard — the weak handle would upgrade forever, including after \
117                 sweep frees the target (use-after-free); push a HeapGuard on the entry path",
118                std::any::type_name::<T>()
119            );
120        }
121        let (heap, allocation_token) = match tracked {
122            Some((heap, token)) => (Some(heap), token),
123            None => (None, 0),
124        };
125        GcWeak {
126            target: self.0,
127            identity,
128            allocation_token,
129            heap,
130        }
131    }
132
133    /// Charge (`delta > 0`) or refund (`delta < 0`) bytes of this object's
134    /// owned heap buffers against the active heap's pacer, so collections
135    /// fire at honest memory pressure. No-op on `delta == 0` or when the
136    /// underlying box is uncollected (see [`lua_gc::Gc::account_buffer`]).
137    ///
138    /// A guard-less charge on a *heap-owned* box panics unconditionally in
139    /// every build: the charge would be silently dropped and the pacer would
140    /// drift from real memory. Detached (uncollected, process-lifetime) boxes
141    /// keep the legacy no-op behavior.
142    pub fn account_buffer(&self, delta: isize) {
143        if delta == 0 {
144            return;
145        }
146        lua_gc::with_current_heap(|h| match h {
147            Some(h) => self.0.account_buffer(h, delta),
148            None => {
149                if self.0.is_heap_owned() {
150                    panic!(
151                        "account_buffer({delta}) on a heap-owned \
152                         {} with no active HeapGuard — the charge would be silently dropped \
153                         and the pacer would drift from real memory; push a HeapGuard on \
154                         the entry path",
155                        std::any::type_name::<T>()
156                    );
157                }
158            }
159        })
160    }
161}
162
163/// A weak handle to a `GcRef<T>`.
164#[derive(Debug)]
165pub struct GcWeak<T: Trace + 'static> {
166    target: Gc<T>,
167    identity: usize,
168    allocation_token: usize,
169    heap: Option<HeapRef>,
170}
171
172impl<T: Trace + 'static> GcWeak<T> {
173    /// Try to promote to a strong reference.
174    pub fn upgrade(&self) -> Option<GcRef<T>> {
175        if let Some(heap) = &self.heap {
176            if !heap.contains_allocation(self.identity, self.allocation_token) {
177                return None;
178            }
179        }
180        Some(GcRef(self.target))
181    }
182
183    /// Strong reference count of the target from this weak handle's point of
184    /// view: one while it can still upgrade, zero after sweep.
185    pub fn strong_count(&self) -> usize {
186        usize::from(self.upgrade().is_some())
187    }
188
189    pub fn identity(&self) -> usize {
190        self.identity
191    }
192}
193
194impl<T: Trace + 'static> Clone for GcWeak<T> {
195    fn clone(&self) -> Self {
196        GcWeak {
197            target: self.target,
198            identity: self.identity,
199            allocation_token: self.allocation_token,
200            heap: self.heap.clone(),
201        }
202    }
203}
204
205impl<T: Trace + 'static> Clone for GcRef<T> {
206    fn clone(&self) -> Self {
207        GcRef(self.0)
208    }
209}
210
211impl<T: Trace + 'static> Copy for GcRef<T> {}
212
213impl<T: Trace + 'static> std::ops::Deref for GcRef<T> {
214    type Target = T;
215    fn deref(&self) -> &T {
216        &*self.0
217    }
218}
219
220impl<T: Trace + 'static> AsRef<T> for GcRef<T> {
221    fn as_ref(&self) -> &T {
222        &*self.0
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    struct NoRoots;
231
232    impl Trace for NoRoots {
233        fn trace(&self, _m: &mut Marker) {}
234    }
235
236    #[derive(Debug)]
237    struct Cell0;
238
239    impl Trace for Cell0 {
240        fn trace(&self, _m: &mut Marker) {}
241    }
242
243    #[test]
244    fn heap_tracked_weak_refs_stop_upgrading_after_sweep() {
245        let heap = lua_gc::Heap::new();
246        heap.unpause();
247        let _guard = lua_gc::HeapGuard::push(&heap);
248
249        let strong = GcRef::new(Cell0);
250        let weak = strong.downgrade();
251        assert!(weak.upgrade().is_some());
252        assert_eq!(weak.strong_count(), 1);
253
254        heap.full_collect(&NoRoots);
255        assert!(weak.upgrade().is_none());
256        assert_eq!(weak.strong_count(), 0);
257    }
258
259    /// The detached fallback is gone (#253): a guard-less allocation is a
260    /// bug on the entry path, and it must fail loudly in every build, not
261    /// leak quietly for the life of the process.
262    #[test]
263    #[should_panic(expected = "no active HeapGuard")]
264    fn guardless_allocation_panics() {
265        let _ = GcRef::new(Cell0);
266    }
267
268    /// A guard-less `downgrade` of a heap-owned box mints a `GcWeak` with no
269    /// heap identity, so it upgrades forever — including after sweep frees the
270    /// target (use-after-free). That now panics unconditionally, not just
271    /// under a retired env flag.
272    #[test]
273    #[should_panic(expected = "no active HeapGuard")]
274    fn guardless_downgrade_of_heap_owned_box_panics() {
275        let heap = lua_gc::Heap::new();
276        let strong = {
277            let _guard = lua_gc::HeapGuard::push(&heap);
278            GcRef::new(Cell0)
279        };
280        let _ = strong.downgrade();
281    }
282
283    /// A guard-less `account_buffer` on a heap-owned box would silently drop
284    /// the pacer charge, drifting the collector from real memory. That now
285    /// panics unconditionally, not just under a retired env flag.
286    #[test]
287    #[should_panic(expected = "no active HeapGuard")]
288    fn guardless_account_buffer_on_heap_owned_box_panics() {
289        let heap = lua_gc::Heap::new();
290        let strong = {
291            let _guard = lua_gc::HeapGuard::push(&heap);
292            GcRef::new(Cell0)
293        };
294        strong.account_buffer(64);
295    }
296
297    /// Codex finding 2 on issue #260: downgrading a stale `GcRef` AFTER the
298    /// heap closed (its box already freed by `drop_all`) must yield a weak
299    /// handle that never upgrades. Before the closed-heap token refusal,
300    /// `downgrade` re-registered the freed box's address in the token map and
301    /// the weak handle upgraded into freed memory.
302    #[test]
303    fn downgrade_after_close_cannot_resurrect_freed_box() {
304        let heap = lua_gc::Heap::new();
305        heap.unpause();
306        let _guard = lua_gc::HeapGuard::push(&heap);
307
308        let stale = GcRef::new(Cell0);
309        heap.drop_all();
310
311        let weak = stale.downgrade();
312        assert!(
313            weak.upgrade().is_none(),
314            "a weak handle minted after close must never upgrade — the box \
315             is freed and the heap is closed"
316        );
317    }
318}
319
320impl<T: PartialEq + Trace + 'static> PartialEq for GcRef<T> {
321    fn eq(&self, other: &Self) -> bool {
322        Gc::ptr_eq(self.0, other.0) || **self == **other
323    }
324}