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