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// ──────────────────────────────────────────────────────────────────────────────