Skip to main content

lua_types/
gc.rs

1//! `GcRef<T>` — the GC-managed reference handle.
2//!
3//! Phase A/B/C: thin newtype around `Rc<T>`.
4//! Phase D-1e (current): newtype around `lua_gc::Gc<T>` — Copy under the hood,
5//! tracks allocation in the active `Heap` (via `lua_gc::with_current_heap(...)`).
6//!
7//! Surface kept stable across the swap: `new`, `ptr_eq`, `identity`,
8//! `strong_count`, `weak_count`, `downgrade`. Existing code touching
9//! `gc.0` continues to work — `.0` is now `Gc<T>` instead of `Rc<T>`.
10//!
11//! # Weak refs
12//!
13//! Heap-tracked `GcWeak<T>` handles remember the heap active when they were
14//! created plus the target's heap allocation token. They upgrade only while
15//! that identity/token pair remains live. Handles to legacy uncollected boxes
16//! still upgrade forever, matching their process-lifetime allocation model.
17
18use lua_gc::{Gc, HeapRef, Marker, Trace};
19
20/// A GC-managed pointer to a Lua collectable object. Newtype over
21/// `lua_gc::Gc<T>` so callers preserve `gc.0`-shape access while the
22/// backend swaps under them.
23#[derive(Debug)]
24pub struct GcRef<T: Trace + 'static>(pub Gc<T>);
25
26impl<T: Trace + 'static> GcRef<T> {
27    /// Allocate a new GC-tracked value on the active heap: joins the allgc
28    /// chain under a `HeapGuard` (set by `state.run()` / `pcall_k` /
29    /// `with_state`), or the heap's bootstrap list inside a bootstrap
30    /// window.
31    ///
32    /// Panics if no heap is active. There is no fallback: the old detached
33    /// arm allocated a box no heap ever freed (issue #249's leak class), and
34    /// since #253 moved host-side `LuaError` messages to owned bytes, no
35    /// legitimate guard-less allocation path remains — a panic here is
36    /// always a missing guard on the entry path, and the message says so.
37    pub fn new(value: T) -> Self {
38        let gc = lua_gc::with_current_heap(|heap| match heap {
39            Some(heap) => heap.allocate(value),
40            None => panic!(
41                "GcRef::new::<{}> with no active HeapGuard — a detached allocation \
42                 would never be freed (issue #249 class); push a HeapGuard or \
43                 bootstrap window on the entry path",
44                std::any::type_name::<T>()
45            ),
46        });
47        GcRef(gc)
48    }
49
50    /// Cycle-aware trace dispatch.
51    ///
52    /// During D-0/D-1 (before a real Color::Gray flag is reachable from
53    /// inside Trace impls), `Marker::try_visit` records the underlying
54    /// allocation's identity and short-circuits a second recursion. Once
55    /// D-2 ships the in-header color flag, this helper collapses to
56    /// `m.mark(self.0)`.
57    pub fn trace_obj(&self, m: &mut Marker) {
58        if m.try_visit(self.identity()) {
59            (**self).trace(m);
60        }
61    }
62}
63
64impl<T: Trace + 'static> GcRef<T> {
65    /// Two `GcRef`s are identity-equal iff they point at the same box.
66    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
67        Gc::ptr_eq(a.0, b.0)
68    }
69
70    /// Identity as a usize — used as a HashMap key for "same object" tests.
71    pub fn identity(&self) -> usize {
72        self.0.identity()
73    }
74
75    /// Number of strong references. `GcRef` is not reference-counted, so a live
76    /// handle reports one owning GC reachability handle.
77    pub fn strong_count(&self) -> usize {
78        1
79    }
80
81    /// Number of weak references. Weak handles are not counted.
82    pub fn weak_count(&self) -> usize {
83        0
84    }
85
86    /// Get a weak handle. If this allocation belongs to the currently-active
87    /// heap, the weak handle will stop upgrading once sweep removes that exact
88    /// heap allocation.
89    ///
90    /// Downgrading a *heap-owned* box with no active `HeapGuard` is a
91    /// latent use-after-free: the resulting handle has no heap identity to
92    /// check, so it keeps upgrading after sweep frees the target.
93    /// `OMNILUA_GC_STRICT_GUARD=1` turns that case into a panic; detached
94    /// (process-lifetime) targets keep the legacy always-upgrades behavior.
95    pub fn downgrade(&self) -> GcWeak<T> {
96        let identity = self.identity();
97        let tracked = lua_gc::with_current_heap(|heap| {
98            heap.map(|heap| {
99                let token = heap.register_allocation_token(identity);
100                (HeapRef::from_heap(heap), token)
101            })
102        });
103        if tracked.is_none() && lua_gc::strict_guard_mode() && self.0.is_heap_owned() {
104            panic!(
105                "OMNILUA_GC_STRICT_GUARD: GcRef::downgrade::<{}> on a heap-owned box with no \
106                 active HeapGuard — the weak handle would upgrade forever, including after \
107                 sweep frees the target (use-after-free); push a HeapGuard on the entry path",
108                std::any::type_name::<T>()
109            );
110        }
111        let (heap, allocation_token) = match tracked {
112            Some((heap, token)) => (Some(heap), token),
113            None => (None, 0),
114        };
115        GcWeak {
116            target: self.0,
117            identity,
118            allocation_token,
119            heap,
120        }
121    }
122
123    /// Charge (`delta > 0`) or refund (`delta < 0`) bytes of this object's
124    /// owned heap buffers against the active heap's pacer, so collections
125    /// fire at honest memory pressure. No-op on `delta == 0`, when no heap is
126    /// active, or when the underlying box is uncollected (see
127    /// [`lua_gc::Gc::account_buffer`]).
128    pub fn account_buffer(&self, delta: isize) {
129        if delta == 0 {
130            return;
131        }
132        lua_gc::with_current_heap(|h| match h {
133            Some(h) => self.0.account_buffer(h, delta),
134            None => {
135                if lua_gc::strict_guard_mode() && self.0.is_heap_owned() {
136                    panic!(
137                        "OMNILUA_GC_STRICT_GUARD: account_buffer({delta}) on a heap-owned \
138                         {} with no active HeapGuard — the charge would be silently dropped \
139                         and the pacer would drift from real memory; push a HeapGuard on \
140                         the entry path",
141                        std::any::type_name::<T>()
142                    );
143                }
144            }
145        })
146    }
147}
148
149/// A weak handle to a `GcRef<T>`.
150#[derive(Debug)]
151pub struct GcWeak<T: Trace + 'static> {
152    target: Gc<T>,
153    identity: usize,
154    allocation_token: usize,
155    heap: Option<HeapRef>,
156}
157
158impl<T: Trace + 'static> GcWeak<T> {
159    /// Try to promote to a strong reference.
160    pub fn upgrade(&self) -> Option<GcRef<T>> {
161        if let Some(heap) = &self.heap {
162            if !heap.contains_allocation(self.identity, self.allocation_token) {
163                return None;
164            }
165        }
166        Some(GcRef(self.target))
167    }
168
169    /// Strong reference count of the target from this weak handle's point of
170    /// view: one while it can still upgrade, zero after sweep.
171    pub fn strong_count(&self) -> usize {
172        usize::from(self.upgrade().is_some())
173    }
174
175    pub fn identity(&self) -> usize {
176        self.identity
177    }
178}
179
180impl<T: Trace + 'static> Clone for GcWeak<T> {
181    fn clone(&self) -> Self {
182        GcWeak {
183            target: self.target,
184            identity: self.identity,
185            allocation_token: self.allocation_token,
186            heap: self.heap.clone(),
187        }
188    }
189}
190
191impl<T: Trace + 'static> Clone for GcRef<T> {
192    fn clone(&self) -> Self {
193        GcRef(self.0)
194    }
195}
196
197impl<T: Trace + 'static> Copy for GcRef<T> {}
198
199impl<T: Trace + 'static> std::ops::Deref for GcRef<T> {
200    type Target = T;
201    fn deref(&self) -> &T {
202        &*self.0
203    }
204}
205
206impl<T: Trace + 'static> AsRef<T> for GcRef<T> {
207    fn as_ref(&self) -> &T {
208        &*self.0
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    struct NoRoots;
217
218    impl Trace for NoRoots {
219        fn trace(&self, _m: &mut Marker) {}
220    }
221
222    #[derive(Debug)]
223    struct Cell0;
224
225    impl Trace for Cell0 {
226        fn trace(&self, _m: &mut Marker) {}
227    }
228
229    #[test]
230    fn heap_tracked_weak_refs_stop_upgrading_after_sweep() {
231        let heap = lua_gc::Heap::new();
232        heap.unpause();
233        let _guard = lua_gc::HeapGuard::push(&heap);
234
235        let strong = GcRef::new(Cell0);
236        let weak = strong.downgrade();
237        assert!(weak.upgrade().is_some());
238        assert_eq!(weak.strong_count(), 1);
239
240        heap.full_collect(&NoRoots);
241        assert!(weak.upgrade().is_none());
242        assert_eq!(weak.strong_count(), 0);
243    }
244
245    /// The detached fallback is gone (#253): a guard-less allocation is a
246    /// bug on the entry path, and it must fail loudly in every build, not
247    /// leak quietly for the life of the process.
248    #[test]
249    #[should_panic(expected = "no active HeapGuard")]
250    fn guardless_allocation_panics() {
251        let _ = GcRef::new(Cell0);
252    }
253}
254
255impl<T: PartialEq + Trace + 'static> PartialEq for GcRef<T> {
256    fn eq(&self, other: &Self) -> bool {
257        Gc::ptr_eq(self.0, other.0) || **self == **other
258    }
259}
260
261// ──────────────────────────────────────────────────────────────────────────────
262// PORT STATUS
263//   source:        n/a (GcRef public wrapper around lua-gc::Gc<T>)
264//   target_crate:  lua-types
265//   confidence:    high
266//   todos:         0
267//   port_notes:    0
268//   unsafe_blocks: 0
269//   notes:         Thin wrapper type so consumers across crates don't depend on lua-gc's
270//                  raw Gc<T>. Clone/Deref/PartialEq forwarded; no unsafe surface.
271// ──────────────────────────────────────────────────────────────────────────────