Skip to main content

miniextendr_api/
refcount_protect.rs

1//! Reference-counted GC protection using a `BTreeMap` + VECSXP backing.
2//!
3//! This module provides an alternative to [`gc_protect`](crate::gc_protect) that uses
4//! reference counting instead of R's LIFO protect stack. This allows releasing
5//! protections in any order and avoids the `--max-ppsize` limit.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────┐
11//! │  RefCountedArena / ThreadLocalArena                                 │
12//! │  ┌─────────────────────────┐   ┌───────────────────────────────────┐│
13//! │  │  BTreeMap<usize, Entry> │   │  VECSXP (R_PreserveObject'd)      ││
14//! │  │  ────────────────────── │   │  ─────────────────────────────    ││
15//! │  │  sexp_a → {count:2, i:0}│◄──┤  [0]: sexp_a                      ││
16//! │  │  sexp_b → {count:1, i:1}│◄──┤  [1]: sexp_b                      ││
17//! │  │  sexp_c → {count:1, i:2}│◄──┤  [2]: sexp_c                      ││
18//! │  └─────────────────────────┘   │  [3]: <free>                      ││
19//! │                                └───────────────────────────────────┘│
20//! └─────────────────────────────────────────────────────────────────────┘
21//! ```
22//!
23//! # Available Types
24//!
25//! | Type | Storage | Use Case |
26//! |------|---------|----------|
27//! | [`RefCountedArena`] | `RefCell` | General purpose, ordered, ppsize-scale workloads |
28//! | [`ThreadLocalArena`] | `thread_local` | Lowest overhead — no `RefCell` borrow checking |
29//!
30//! Only these two flavors are instantiated anywhere in the tree today. This
31//! module previously also shipped HashMap- and ahash-backed variants
32//! (`HashMapArena`, `ThreadLocalHashArena`, `FastHashMapArena`,
33//! `ThreadLocalFastHashArena`) behind a generic `MapStorage` abstraction; they
34//! were removed for having zero production or test consumers (see
35//! <https://github.com/a2-ai/miniextendr/issues/ISSUE_NUMBER_PLACEHOLDER>).
36//! Re-add a flavor (and the generic `MapStorage` plumbing needed to support
37//! more than one map type again) when a real consumer appears.
38
39use crate::sys::{R_PreserveObject, R_ReleaseObject, Rf_allocVector, Rf_protect, Rf_unprotect};
40use crate::{R_xlen_t, SEXP, SEXPTYPE, SexpExt};
41use std::cell::RefCell;
42use std::collections::BTreeMap;
43use std::marker::PhantomData;
44use std::mem::MaybeUninit;
45use std::rc::Rc;
46
47// region: Entry type
48
49/// Entry in the reference count map.
50#[derive(Debug, Clone, Copy)]
51struct Entry {
52    /// Reference count (how many times this SEXP has been protected)
53    count: usize,
54    /// Index in the backing VECSXP
55    index: usize,
56}
57// endregion
58
59// region: Core arena state (shared between RefCell and thread_local variants)
60
61/// Core arena state without interior mutability.
62///
63/// This is used internally by both [`RefCountedArena`] (with `RefCell`) and
64/// [`ThreadLocalArena`] (with `UnsafeCell`).
65struct ArenaState {
66    /// Map from SEXP pointer to entry
67    map: MaybeUninit<BTreeMap<usize, Entry>>,
68    /// Backing VECSXP (preserved via R_PreserveObject)
69    backing: SEXP,
70    /// Current capacity
71    capacity: usize,
72    /// Number of active entries
73    len: usize,
74    /// Free list for slot reuse
75    free_list: Vec<usize>,
76    /// Monotonic write cursor: next index to hand out on the fresh-slot path.
77    /// Distinct from `len` (live count) — after release cycles `len` can be
78    /// lower than the highest index ever handed out, so using `len` as the
79    /// cursor can return an index that is already on the free-list.
80    next_slot: usize,
81}
82
83impl ArenaState {
84    /// Initial capacity for the backing VECSXP.
85    ///
86    /// This is suitable for light usage (a handful of protected values).
87    /// For ppsize-scale workloads (hundreds or thousands of protected values),
88    /// use [`RefCountedArena::with_capacity`] or
89    /// [`ThreadLocalArena::init_with_capacity`] to avoid repeated backing
90    /// VECSXP growth and map rehashing.
91    const INITIAL_CAPACITY: usize = 16;
92
93    /// Maximum capacity: the backing VECSXP is indexed by `R_xlen_t` (isize),
94    /// so the capacity must fit in a non-negative `R_xlen_t`.
95    const MAX_CAPACITY: usize = R_xlen_t::MAX as usize;
96
97    /// Convert a `usize` capacity to `R_xlen_t`, panicking on overflow.
98    #[inline]
99    fn capacity_as_r_xlen(cap: usize) -> R_xlen_t {
100        R_xlen_t::try_from(cap).unwrap_or_else(|_| {
101            panic!(
102                "arena capacity {} exceeds R_xlen_t::MAX ({})",
103                cap,
104                R_xlen_t::MAX
105            )
106        })
107    }
108
109    /// Create uninitialized state (for thread_local).
110    const fn uninit() -> Self {
111        Self {
112            map: MaybeUninit::uninit(),
113            backing: SEXP(std::ptr::null_mut()),
114            capacity: 0,
115            len: 0,
116            free_list: Vec::new(),
117            next_slot: 0,
118        }
119    }
120
121    /// Initialize the state.
122    ///
123    /// # Safety
124    ///
125    /// Must be called exactly once before using the state.
126    unsafe fn init(&mut self, capacity: usize) {
127        let capacity = capacity.max(1);
128        assert!(
129            capacity <= Self::MAX_CAPACITY,
130            "arena capacity {} exceeds R_xlen_t::MAX ({})",
131            capacity,
132            R_xlen_t::MAX
133        );
134        unsafe {
135            let r_cap = Self::capacity_as_r_xlen(capacity);
136            let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
137            R_PreserveObject(backing);
138            Rf_unprotect(1);
139
140            self.map.write(BTreeMap::new());
141            self.backing = backing;
142            self.capacity = capacity;
143            self.len = 0;
144            self.next_slot = 0;
145            self.free_list = Vec::with_capacity(capacity);
146        }
147    }
148
149    /// Create initialized state.
150    unsafe fn new(capacity: usize) -> Self {
151        let capacity = capacity.max(1);
152        let mut state = Self {
153            map: MaybeUninit::new(BTreeMap::new()),
154            backing: SEXP(std::ptr::null_mut()),
155            capacity: 0,
156            len: 0,
157            next_slot: 0,
158            free_list: Vec::with_capacity(capacity),
159        };
160        unsafe { state.init_backing(capacity) };
161        state
162    }
163
164    /// Initialize just the backing (map already initialized).
165    unsafe fn init_backing(&mut self, capacity: usize) {
166        let capacity = capacity.max(1);
167        assert!(
168            capacity <= Self::MAX_CAPACITY,
169            "arena capacity {} exceeds R_xlen_t::MAX ({})",
170            capacity,
171            R_xlen_t::MAX
172        );
173        unsafe {
174            let r_cap = Self::capacity_as_r_xlen(capacity);
175            let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
176            R_PreserveObject(backing);
177            Rf_unprotect(1);
178
179            self.backing = backing;
180            self.capacity = capacity;
181        }
182    }
183
184    /// Get a reference to the map.
185    #[inline]
186    fn map(&self) -> &BTreeMap<usize, Entry> {
187        // SAFETY: Map is initialized before any access
188        unsafe { self.map.assume_init_ref() }
189    }
190
191    /// Get a mutable reference to the map.
192    #[inline]
193    fn map_mut(&mut self) -> &mut BTreeMap<usize, Entry> {
194        // SAFETY: Map is initialized before any access
195        unsafe { self.map.assume_init_mut() }
196    }
197
198    /// Decrement the count for a key and remove if zero.
199    ///
200    /// Returns `Some((true, index))` if the entry was found and removed,
201    /// `Some((false, index))` if the entry was found but count > 0 after
202    /// decrement, `None` if the entry was not found.
203    #[inline]
204    fn decrement_and_maybe_remove(&mut self, key: &usize) -> Option<(bool, usize)> {
205        let map = self.map_mut();
206        if let Some(entry) = map.get_mut(key) {
207            entry.count -= 1;
208            if entry.count == 0 {
209                let index = entry.index;
210                map.remove(key);
211                Some((true, index))
212            } else {
213                Some((false, entry.index))
214            }
215        } else {
216            None
217        }
218    }
219
220    /// Protect a SEXP from garbage collection.
221    ///
222    /// # Safety
223    ///
224    /// Must be called from the R main thread. The SEXP must be valid.
225    #[inline]
226    unsafe fn protect(&mut self, x: SEXP) -> SEXP {
227        if x.is_nil() {
228            return x;
229        }
230
231        let key = x.0 as usize;
232
233        if let Some(entry) = self.map_mut().get_mut(&key) {
234            entry.count += 1;
235        } else {
236            let index = self.allocate_slot();
237            self.backing.set_vector_elt(index as R_xlen_t, x);
238            self.map_mut().insert(key, Entry { count: 1, index });
239            self.len += 1;
240        }
241
242        x
243    }
244
245    /// Unprotect a SEXP, allowing garbage collection when refcount reaches zero.
246    ///
247    /// # Safety
248    ///
249    /// Must be called from the R main thread. The SEXP must have been
250    /// previously protected by this arena.
251    #[inline]
252    unsafe fn unprotect(&mut self, x: SEXP) {
253        if x.is_nil() {
254            return;
255        }
256
257        let key = x.0 as usize;
258
259        match self.decrement_and_maybe_remove(&key) {
260            Some((true, index)) => {
261                // Entry was removed (count reached 0)
262                self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
263                self.free_list.push(index);
264                self.len -= 1;
265            }
266            Some((false, _)) => {
267                // Entry still exists (count > 0)
268            }
269            None => {
270                panic!("unprotect called on SEXP not protected by this arena");
271            }
272        }
273    }
274
275    /// Try to unprotect a SEXP. Returns false if not protected by this arena.
276    ///
277    /// # Safety
278    ///
279    /// Must be called from the R main thread.
280    #[inline]
281    unsafe fn try_unprotect(&mut self, x: SEXP) -> bool {
282        if x.is_nil() {
283            return false;
284        }
285
286        let key = x.0 as usize;
287
288        match self.decrement_and_maybe_remove(&key) {
289            Some((true, index)) => {
290                // Entry was removed (count reached 0)
291                self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
292                self.free_list.push(index);
293                self.len -= 1;
294                true
295            }
296            Some((false, _)) => {
297                // Entry still exists (count > 0)
298                true
299            }
300            None => false,
301        }
302    }
303
304    #[inline]
305    /// Returns true if this arena currently protects `x`.
306    fn is_protected(&self, x: SEXP) -> bool {
307        if x.is_nil() {
308            return false;
309        }
310        let key = x.0 as usize;
311        self.map().contains_key(&key)
312    }
313
314    #[inline]
315    /// Returns the current reference count for `x` in this arena.
316    ///
317    /// Returns 0 if `x` is not protected or is `SEXP::nil()`.
318    fn ref_count(&self, x: SEXP) -> usize {
319        if x.is_nil() {
320            return 0;
321        }
322        let key = x.0 as usize;
323        self.map().get(&key).map(|e| e.count).unwrap_or(0)
324    }
325
326    fn allocate_slot(&mut self) -> usize {
327        if let Some(index) = self.free_list.pop() {
328            return index;
329        }
330
331        if self.next_slot >= self.capacity {
332            unsafe { self.grow() };
333        }
334
335        let idx = self.next_slot;
336        self.next_slot += 1;
337        idx
338    }
339
340    unsafe fn grow(&mut self) {
341        let old_capacity = self.capacity;
342        let new_capacity = old_capacity
343            .checked_mul(2)
344            .expect("arena capacity overflow during growth");
345        assert!(
346            new_capacity <= Self::MAX_CAPACITY,
347            "arena capacity {} would exceed R_xlen_t::MAX ({}) after growth",
348            new_capacity,
349            R_xlen_t::MAX
350        );
351        let old_backing = self.backing;
352
353        unsafe {
354            let r_new_cap = Self::capacity_as_r_xlen(new_capacity);
355            let new_backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_new_cap));
356            R_PreserveObject(new_backing);
357
358            for i in 0..old_capacity {
359                let r_i = Self::capacity_as_r_xlen(i);
360                let elt = old_backing.vector_elt(r_i);
361                new_backing.set_vector_elt(r_i, elt);
362            }
363
364            R_ReleaseObject(old_backing);
365            Rf_unprotect(1);
366
367            self.backing = new_backing;
368            self.capacity = new_capacity;
369        }
370    }
371
372    /// Clear all protected values from the arena.
373    ///
374    /// # Safety
375    ///
376    /// Must be called from the R main thread.
377    unsafe fn clear(&mut self) {
378        for entry in self.map().values() {
379            self.backing
380                .set_vector_elt(entry.index as R_xlen_t, SEXP::nil());
381        }
382        self.map_mut().clear();
383        self.free_list.clear();
384        self.len = 0;
385        self.next_slot = 0;
386    }
387
388    unsafe fn release_backing(&mut self) {
389        if !self.backing.0.is_null() {
390            unsafe { R_ReleaseObject(self.backing) };
391            self.backing = SEXP(std::ptr::null_mut());
392        }
393    }
394}
395// endregion
396
397// region: RefCountedArena - RefCell-based arena
398
399/// Enforces `!Send + !Sync` (R API is not thread-safe).
400type NoSendSync = PhantomData<Rc<()>>;
401
402/// A reference-counted arena for GC protection, backed by a `BTreeMap`.
403///
404/// This provides an alternative to R's PROTECT stack that:
405/// - Uses reference counting for each SEXP
406/// - Allows releasing protections in any order
407/// - Has no stack size limit (uses heap allocation)
408pub struct RefCountedArena {
409    state: RefCell<ArenaState>,
410    _nosend: NoSendSync,
411}
412
413impl RefCountedArena {
414    /// Create a new arena with default capacity (16 slots).
415    ///
416    /// For workloads protecting many distinct SEXPs (e.g., ppsize-scale loops),
417    /// prefer [`with_capacity`](Self::with_capacity) to avoid backing VECSXP
418    /// growth and map rehashing during operation.
419    ///
420    /// # Safety
421    ///
422    /// Must be called from the R main thread.
423    pub unsafe fn new() -> Self {
424        unsafe { Self::with_capacity(ArenaState::INITIAL_CAPACITY) }
425    }
426
427    /// Create a new arena with specific initial capacity.
428    ///
429    /// Pre-sizing the arena avoids growth of the backing VECSXP and rehashing
430    /// of the internal map. Use this when the expected number of distinct
431    /// protected values is known or can be estimated.
432    ///
433    /// # Safety
434    ///
435    /// Must be called from the R main thread.
436    pub unsafe fn with_capacity(capacity: usize) -> Self {
437        Self {
438            state: RefCell::new(unsafe { ArenaState::new(capacity) }),
439            _nosend: PhantomData,
440        }
441    }
442
443    /// Protect a SEXP, incrementing its reference count.
444    ///
445    /// # Safety
446    ///
447    /// Must be called from the R main thread.
448    #[inline]
449    pub unsafe fn protect(&self, x: SEXP) -> SEXP {
450        unsafe { self.state.borrow_mut().protect(x) }
451    }
452
453    /// Unprotect a SEXP, decrementing its reference count.
454    ///
455    /// # Safety
456    ///
457    /// Must be called from the R main thread.
458    ///
459    /// # Panics
460    ///
461    /// Panics if `x` was not protected by this arena.
462    #[inline]
463    pub unsafe fn unprotect(&self, x: SEXP) {
464        unsafe { self.state.borrow_mut().unprotect(x) };
465    }
466
467    /// Try to unprotect a SEXP, returning `true` if it was protected.
468    ///
469    /// # Safety
470    ///
471    /// Must be called from the R main thread.
472    #[inline]
473    pub unsafe fn try_unprotect(&self, x: SEXP) -> bool {
474        unsafe { self.state.borrow_mut().try_unprotect(x) }
475    }
476
477    /// Check if a SEXP is currently protected by this arena.
478    #[inline]
479    pub fn is_protected(&self, x: SEXP) -> bool {
480        self.state.borrow().is_protected(x)
481    }
482
483    /// Get the reference count for a SEXP (0 if not protected).
484    #[inline]
485    pub fn ref_count(&self, x: SEXP) -> usize {
486        self.state.borrow().ref_count(x)
487    }
488
489    /// Get the number of distinct SEXPs currently protected.
490    #[inline]
491    pub fn len(&self) -> usize {
492        self.state.borrow().len
493    }
494
495    /// Check if the arena is empty.
496    #[inline]
497    pub fn is_empty(&self) -> bool {
498        self.state.borrow().len == 0
499    }
500
501    /// Get the current capacity.
502    #[inline]
503    pub fn capacity(&self) -> usize {
504        self.state.borrow().capacity
505    }
506
507    /// Clear all protections.
508    ///
509    /// # Safety
510    ///
511    /// Must be called from the R main thread.
512    pub unsafe fn clear(&self) {
513        unsafe { self.state.borrow_mut().clear() };
514    }
515
516    /// Protect a SEXP and return an RAII guard.
517    ///
518    /// # Safety
519    ///
520    /// Must be called from the R main thread.
521    #[inline]
522    pub unsafe fn guard(&self, x: SEXP) -> ArenaGuard<'_> {
523        unsafe { ArenaGuard::new(self, x) }
524    }
525}
526
527impl Drop for RefCountedArena {
528    fn drop(&mut self) {
529        let state = self.state.get_mut();
530        // SAFETY: RefCountedArena always constructs via ArenaState::new(),
531        // which initializes the map.
532        unsafe { state.map.assume_init_drop() };
533        unsafe { state.release_backing() };
534    }
535}
536
537impl Default for RefCountedArena {
538    fn default() -> Self {
539        unsafe { Self::new() }
540    }
541}
542// endregion
543
544// region: RAII Guard
545
546/// An RAII guard that unprotects a SEXP when dropped.
547pub struct ArenaGuard<'a> {
548    arena: &'a RefCountedArena,
549    sexp: SEXP,
550}
551
552impl<'a> ArenaGuard<'a> {
553    /// Create a new guard that protects the SEXP and unprotects on drop.
554    ///
555    /// # Safety
556    ///
557    /// Must be called from the R main thread. The SEXP must be valid.
558    #[inline]
559    pub unsafe fn new(arena: &'a RefCountedArena, sexp: SEXP) -> Self {
560        unsafe { arena.protect(sexp) };
561        Self { arena, sexp }
562    }
563
564    #[inline]
565    /// Returns the protected SEXP.
566    pub fn get(&self) -> SEXP {
567        self.sexp
568    }
569}
570
571impl Drop for ArenaGuard<'_> {
572    fn drop(&mut self) {
573        unsafe { self.arena.unprotect(self.sexp) };
574    }
575}
576
577impl std::ops::Deref for ArenaGuard<'_> {
578    type Target = SEXP;
579
580    #[inline]
581    fn deref(&self) -> &Self::Target {
582        &self.sexp
583    }
584}
585// endregion
586
587// region: ThreadLocalArena
588
589/// State wrapper for the thread-local arena.
590struct ThreadLocalState {
591    inner: ArenaState,
592    initialized: bool,
593}
594
595impl ThreadLocalState {
596    /// Create an uninitialized thread-local arena state.
597    ///
598    /// Call `init` or `init_with_capacity` before use.
599    const fn uninit() -> Self {
600        Self {
601            inner: ArenaState::uninit(),
602            initialized: false,
603        }
604    }
605
606    /// Initialize with default capacity (16 slots).
607    ///
608    /// For ppsize-scale workloads, prefer [`init_with_capacity`](Self::init_with_capacity)
609    /// to avoid backing VECSXP growth and map rehashing during operation.
610    ///
611    /// # Safety
612    ///
613    /// Must be called from the R main thread. Must only be called once.
614    unsafe fn init(&mut self) {
615        unsafe { self.inner.init(ArenaState::INITIAL_CAPACITY) };
616        self.initialized = true;
617    }
618
619    /// Initialize with specific capacity.
620    ///
621    /// Pre-sizing avoids growth of the backing VECSXP and rehashing of the
622    /// internal map. Use this when the expected number of distinct protected
623    /// values is known or can be estimated (e.g., the length of an input vector).
624    ///
625    /// # Safety
626    ///
627    /// Must be called from the R main thread. Must only be called once.
628    unsafe fn init_with_capacity(&mut self, capacity: usize) {
629        unsafe { self.inner.init(capacity) };
630        self.initialized = true;
631    }
632}
633
634impl Drop for ThreadLocalState {
635    fn drop(&mut self) {
636        if self.initialized {
637            // SAFETY: The map was initialized in init() or init_with_capacity().
638            // We must manually drop it because MaybeUninit does not run Drop.
639            unsafe { self.inner.map.assume_init_drop() };
640        }
641        // R backing is released separately via release_backing() if needed.
642        // Thread-local destructors may run after R has shut down, so we do NOT
643        // call R_ReleaseObject here — the R runtime owns the backing VECSXP
644        // lifetime via R_PreserveObject.
645    }
646}
647
648thread_local! {
649    static THREAD_LOCAL_STATE: std::cell::UnsafeCell<ThreadLocalState> =
650        const { std::cell::UnsafeCell::new(ThreadLocalState::uninit()) };
651}
652
653/// Thread-local, `BTreeMap`-backed reference-counted GC protection arena.
654///
655/// This provides the lowest overhead for protection operations by
656/// eliminating `RefCell` borrow checking — each thread gets its own
657/// `ThreadLocalState` (private) accessed through an `UnsafeCell`.
658///
659/// ```ignore
660/// use miniextendr_api::refcount_protect::ThreadLocalArena;
661/// unsafe { ThreadLocalArena::protect(x) };
662/// ```
663pub struct ThreadLocalArena;
664
665impl ThreadLocalArena {
666    /// Access the thread-local state.
667    #[inline]
668    fn with_state<R, F: FnOnce(&mut ThreadLocalState) -> R>(f: F) -> R {
669        THREAD_LOCAL_STATE.with(|cell| f(unsafe { &mut *cell.get() }))
670    }
671
672    /// Initialize the arena with default capacity (called automatically on first use).
673    ///
674    /// # Safety
675    ///
676    /// Must be called from the R main thread.
677    pub unsafe fn init() {
678        Self::with_state(|s| {
679            if !s.initialized {
680                unsafe { s.init() };
681            }
682        });
683    }
684
685    /// Initialize the arena with specific capacity.
686    ///
687    /// Use this when you know the expected number of distinct protected values
688    /// to avoid backing VECSXP growth and map rehashing during operation.
689    ///
690    /// If already initialized, this is a no-op.
691    ///
692    /// # Safety
693    ///
694    /// Must be called from the R main thread.
695    pub unsafe fn init_with_capacity(capacity: usize) {
696        Self::with_state(|s| {
697            if !s.initialized {
698                unsafe { s.init_with_capacity(capacity) };
699            }
700        });
701    }
702
703    /// Protect a SEXP, incrementing its reference count.
704    ///
705    /// # Safety
706    ///
707    /// Must be called from the R main thread.
708    #[inline]
709    pub unsafe fn protect(x: SEXP) -> SEXP {
710        Self::with_state(|s| {
711            if !s.initialized {
712                unsafe { s.init() };
713            }
714            unsafe { s.inner.protect(x) }
715        })
716    }
717
718    /// Unprotect a SEXP.
719    ///
720    /// # Safety
721    ///
722    /// Must be called from the R main thread.
723    #[inline]
724    pub unsafe fn unprotect(x: SEXP) {
725        Self::with_state(|s| {
726            // If the arena was never initialized, no SEXP could have been
727            // protected by it, so there is nothing to unprotect.
728            if !s.initialized {
729                return;
730            }
731            unsafe { s.inner.unprotect(x) };
732        });
733    }
734
735    /// Try to unprotect a SEXP.
736    ///
737    /// # Safety
738    ///
739    /// Must be called from the R main thread.
740    #[inline]
741    pub unsafe fn try_unprotect(x: SEXP) -> bool {
742        Self::with_state(|s| {
743            // If the arena was never initialized, no SEXP could have been
744            // protected by it, so return false.
745            if !s.initialized {
746                return false;
747            }
748            unsafe { s.inner.try_unprotect(x) }
749        })
750    }
751
752    /// Protect without checking initialization.
753    ///
754    /// For hot loops where `init()` or `init_with_capacity()` has already been called.
755    ///
756    /// # Safety
757    ///
758    /// - Must be called from the R main thread.
759    /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
760    #[inline]
761    pub unsafe fn protect_fast(x: SEXP) -> SEXP {
762        Self::with_state(|s| {
763            debug_assert!(s.initialized, "protect_fast called before init");
764            unsafe { s.inner.protect(x) }
765        })
766    }
767
768    /// Unprotect without checking initialization.
769    ///
770    /// For hot loops where `init()` or `init_with_capacity()` has already been called.
771    ///
772    /// # Safety
773    ///
774    /// - Must be called from the R main thread.
775    /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
776    #[inline]
777    pub unsafe fn unprotect_fast(x: SEXP) {
778        Self::with_state(|s| {
779            debug_assert!(s.initialized, "unprotect_fast called before init");
780            unsafe { s.inner.unprotect(x) };
781        });
782    }
783
784    /// Try to unprotect without checking initialization.
785    ///
786    /// For hot loops where `init()` or `init_with_capacity()` has already been called.
787    ///
788    /// # Safety
789    ///
790    /// - Must be called from the R main thread.
791    /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
792    #[inline]
793    pub unsafe fn try_unprotect_fast(x: SEXP) -> bool {
794        Self::with_state(|s| {
795            debug_assert!(s.initialized, "try_unprotect_fast called before init");
796            unsafe { s.inner.try_unprotect(x) }
797        })
798    }
799
800    /// Check if a SEXP is protected.
801    #[inline]
802    pub fn is_protected(x: SEXP) -> bool {
803        Self::with_state(|s| {
804            if !s.initialized {
805                return false;
806            }
807            s.inner.is_protected(x)
808        })
809    }
810
811    /// Get reference count.
812    #[inline]
813    pub fn ref_count(x: SEXP) -> usize {
814        Self::with_state(|s| {
815            if !s.initialized {
816                return 0;
817            }
818            s.inner.ref_count(x)
819        })
820    }
821
822    /// Number of protected SEXPs.
823    #[inline]
824    pub fn len() -> usize {
825        Self::with_state(|s| s.inner.len)
826    }
827
828    /// Check if empty.
829    #[inline]
830    pub fn is_empty() -> bool {
831        Self::len() == 0
832    }
833
834    /// Get capacity.
835    #[inline]
836    pub fn capacity() -> usize {
837        Self::with_state(|s| s.inner.capacity)
838    }
839
840    /// Clear all protections.
841    ///
842    /// # Safety
843    ///
844    /// Must be called from the R main thread.
845    pub unsafe fn clear() {
846        Self::with_state(|s| {
847            if s.initialized {
848                unsafe { s.inner.clear() };
849            }
850        });
851    }
852}
853
854// Tests are in tests/refcount_protect.rs (requires R runtime via miniextendr-engine)
855// endregion