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. There is deliberately no
423 /// `Default` impl: constructing an arena allocates an R VECSXP (via
424 /// `Rf_allocVector` / `R_PreserveObject`) and is an unsafe, main-thread-only
425 /// act, not a safe default (#1096).
426 pub unsafe fn new() -> Self {
427 unsafe { Self::with_capacity(ArenaState::INITIAL_CAPACITY) }
428 }
429
430 /// Create a new arena with specific initial capacity.
431 ///
432 /// Pre-sizing the arena avoids growth of the backing VECSXP and rehashing
433 /// of the internal map. Use this when the expected number of distinct
434 /// protected values is known or can be estimated.
435 ///
436 /// # Safety
437 ///
438 /// Must be called from the R main thread.
439 pub unsafe fn with_capacity(capacity: usize) -> Self {
440 Self {
441 state: RefCell::new(unsafe { ArenaState::new(capacity) }),
442 _nosend: PhantomData,
443 }
444 }
445
446 /// Protect a SEXP, incrementing its reference count.
447 ///
448 /// # Safety
449 ///
450 /// Must be called from the R main thread.
451 #[inline]
452 pub unsafe fn protect(&self, x: SEXP) -> SEXP {
453 unsafe { self.state.borrow_mut().protect(x) }
454 }
455
456 /// Unprotect a SEXP, decrementing its reference count.
457 ///
458 /// # Safety
459 ///
460 /// Must be called from the R main thread.
461 ///
462 /// # Panics
463 ///
464 /// Panics if `x` was not protected by this arena.
465 #[inline]
466 pub unsafe fn unprotect(&self, x: SEXP) {
467 unsafe { self.state.borrow_mut().unprotect(x) };
468 }
469
470 /// Try to unprotect a SEXP, returning `true` if it was protected.
471 ///
472 /// # Safety
473 ///
474 /// Must be called from the R main thread.
475 #[inline]
476 pub unsafe fn try_unprotect(&self, x: SEXP) -> bool {
477 unsafe { self.state.borrow_mut().try_unprotect(x) }
478 }
479
480 /// Check if a SEXP is currently protected by this arena.
481 #[inline]
482 pub fn is_protected(&self, x: SEXP) -> bool {
483 self.state.borrow().is_protected(x)
484 }
485
486 /// Get the reference count for a SEXP (0 if not protected).
487 #[inline]
488 pub fn ref_count(&self, x: SEXP) -> usize {
489 self.state.borrow().ref_count(x)
490 }
491
492 /// Get the number of distinct SEXPs currently protected.
493 #[inline]
494 pub fn len(&self) -> usize {
495 self.state.borrow().len
496 }
497
498 /// Check if the arena is empty.
499 #[inline]
500 pub fn is_empty(&self) -> bool {
501 self.state.borrow().len == 0
502 }
503
504 /// Get the current capacity.
505 #[inline]
506 pub fn capacity(&self) -> usize {
507 self.state.borrow().capacity
508 }
509
510 /// Clear all protections.
511 ///
512 /// # Safety
513 ///
514 /// Must be called from the R main thread.
515 pub unsafe fn clear(&self) {
516 unsafe { self.state.borrow_mut().clear() };
517 }
518
519 /// Protect a SEXP and return an RAII guard.
520 ///
521 /// # Safety
522 ///
523 /// Must be called from the R main thread.
524 #[inline]
525 pub unsafe fn guard(&self, x: SEXP) -> ArenaGuard<'_> {
526 unsafe { ArenaGuard::new(self, x) }
527 }
528}
529
530impl Drop for RefCountedArena {
531 fn drop(&mut self) {
532 let state = self.state.get_mut();
533 // SAFETY: RefCountedArena always constructs via ArenaState::new(),
534 // which initializes the map.
535 unsafe { state.map.assume_init_drop() };
536 unsafe { state.release_backing() };
537 }
538}
539
540// endregion
541
542// region: RAII Guard
543
544/// An RAII guard that unprotects a SEXP when dropped.
545pub struct ArenaGuard<'a> {
546 arena: &'a RefCountedArena,
547 sexp: SEXP,
548}
549
550impl<'a> ArenaGuard<'a> {
551 /// Create a new guard that protects the SEXP and unprotects on drop.
552 ///
553 /// # Safety
554 ///
555 /// Must be called from the R main thread. The SEXP must be valid.
556 #[inline]
557 pub unsafe fn new(arena: &'a RefCountedArena, sexp: SEXP) -> Self {
558 unsafe { arena.protect(sexp) };
559 Self { arena, sexp }
560 }
561
562 #[inline]
563 /// Returns the protected SEXP.
564 pub fn get(&self) -> SEXP {
565 self.sexp
566 }
567}
568
569impl Drop for ArenaGuard<'_> {
570 fn drop(&mut self) {
571 unsafe { self.arena.unprotect(self.sexp) };
572 }
573}
574
575impl std::ops::Deref for ArenaGuard<'_> {
576 type Target = SEXP;
577
578 #[inline]
579 fn deref(&self) -> &Self::Target {
580 &self.sexp
581 }
582}
583// endregion
584
585// region: ThreadLocalArena
586
587/// State wrapper for the thread-local arena.
588struct ThreadLocalState {
589 inner: ArenaState,
590 initialized: bool,
591}
592
593impl ThreadLocalState {
594 /// Create an uninitialized thread-local arena state.
595 ///
596 /// Call `init` or `init_with_capacity` before use.
597 const fn uninit() -> Self {
598 Self {
599 inner: ArenaState::uninit(),
600 initialized: false,
601 }
602 }
603
604 /// Initialize with default capacity (16 slots).
605 ///
606 /// For ppsize-scale workloads, prefer [`init_with_capacity`](Self::init_with_capacity)
607 /// to avoid backing VECSXP growth and map rehashing during operation.
608 ///
609 /// # Safety
610 ///
611 /// Must be called from the R main thread. Must only be called once.
612 unsafe fn init(&mut self) {
613 unsafe { self.inner.init(ArenaState::INITIAL_CAPACITY) };
614 self.initialized = true;
615 }
616
617 /// Initialize with specific capacity.
618 ///
619 /// Pre-sizing avoids growth of the backing VECSXP and rehashing of the
620 /// internal map. Use this when the expected number of distinct protected
621 /// values is known or can be estimated (e.g., the length of an input vector).
622 ///
623 /// # Safety
624 ///
625 /// Must be called from the R main thread. Must only be called once.
626 unsafe fn init_with_capacity(&mut self, capacity: usize) {
627 unsafe { self.inner.init(capacity) };
628 self.initialized = true;
629 }
630}
631
632impl Drop for ThreadLocalState {
633 fn drop(&mut self) {
634 if self.initialized {
635 // SAFETY: The map was initialized in init() or init_with_capacity().
636 // We must manually drop it because MaybeUninit does not run Drop.
637 unsafe { self.inner.map.assume_init_drop() };
638 }
639 // R backing is released separately via release_backing() if needed.
640 // Thread-local destructors may run after R has shut down, so we do NOT
641 // call R_ReleaseObject here — the R runtime owns the backing VECSXP
642 // lifetime via R_PreserveObject.
643 }
644}
645
646thread_local! {
647 static THREAD_LOCAL_STATE: std::cell::UnsafeCell<ThreadLocalState> =
648 const { std::cell::UnsafeCell::new(ThreadLocalState::uninit()) };
649}
650
651/// Thread-local, `BTreeMap`-backed reference-counted GC protection arena.
652///
653/// This provides the lowest overhead for protection operations by
654/// eliminating `RefCell` borrow checking — each thread gets its own
655/// `ThreadLocalState` (private) accessed through an `UnsafeCell`.
656///
657/// ```ignore
658/// use miniextendr_api::refcount_protect::ThreadLocalArena;
659/// unsafe { ThreadLocalArena::protect(x) };
660/// ```
661pub struct ThreadLocalArena;
662
663impl ThreadLocalArena {
664 /// Access the thread-local state.
665 #[inline]
666 fn with_state<R, F: FnOnce(&mut ThreadLocalState) -> R>(f: F) -> R {
667 THREAD_LOCAL_STATE.with(|cell| f(unsafe { &mut *cell.get() }))
668 }
669
670 /// Initialize the arena with default capacity (called automatically on first use).
671 ///
672 /// # Safety
673 ///
674 /// Must be called from the R main thread.
675 pub unsafe fn init() {
676 Self::with_state(|s| {
677 if !s.initialized {
678 unsafe { s.init() };
679 }
680 });
681 }
682
683 /// Initialize the arena with specific capacity.
684 ///
685 /// Use this when you know the expected number of distinct protected values
686 /// to avoid backing VECSXP growth and map rehashing during operation.
687 ///
688 /// If already initialized, this is a no-op.
689 ///
690 /// # Safety
691 ///
692 /// Must be called from the R main thread.
693 pub unsafe fn init_with_capacity(capacity: usize) {
694 Self::with_state(|s| {
695 if !s.initialized {
696 unsafe { s.init_with_capacity(capacity) };
697 }
698 });
699 }
700
701 /// Protect a SEXP, incrementing its reference count.
702 ///
703 /// # Safety
704 ///
705 /// Must be called from the R main thread.
706 #[inline]
707 pub unsafe fn protect(x: SEXP) -> SEXP {
708 Self::with_state(|s| {
709 if !s.initialized {
710 unsafe { s.init() };
711 }
712 unsafe { s.inner.protect(x) }
713 })
714 }
715
716 /// Unprotect a SEXP.
717 ///
718 /// # Safety
719 ///
720 /// Must be called from the R main thread.
721 #[inline]
722 pub unsafe fn unprotect(x: SEXP) {
723 Self::with_state(|s| {
724 // If the arena was never initialized, no SEXP could have been
725 // protected by it, so there is nothing to unprotect.
726 if !s.initialized {
727 return;
728 }
729 unsafe { s.inner.unprotect(x) };
730 });
731 }
732
733 /// Try to unprotect a SEXP.
734 ///
735 /// # Safety
736 ///
737 /// Must be called from the R main thread.
738 #[inline]
739 pub unsafe fn try_unprotect(x: SEXP) -> bool {
740 Self::with_state(|s| {
741 // If the arena was never initialized, no SEXP could have been
742 // protected by it, so return false.
743 if !s.initialized {
744 return false;
745 }
746 unsafe { s.inner.try_unprotect(x) }
747 })
748 }
749
750 /// Protect without checking initialization.
751 ///
752 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
753 ///
754 /// # Safety
755 ///
756 /// - Must be called from the R main thread.
757 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
758 #[inline]
759 pub unsafe fn protect_fast(x: SEXP) -> SEXP {
760 Self::with_state(|s| {
761 debug_assert!(s.initialized, "protect_fast called before init");
762 unsafe { s.inner.protect(x) }
763 })
764 }
765
766 /// Unprotect without checking initialization.
767 ///
768 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
769 ///
770 /// # Safety
771 ///
772 /// - Must be called from the R main thread.
773 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
774 #[inline]
775 pub unsafe fn unprotect_fast(x: SEXP) {
776 Self::with_state(|s| {
777 debug_assert!(s.initialized, "unprotect_fast called before init");
778 unsafe { s.inner.unprotect(x) };
779 });
780 }
781
782 /// Try to unprotect without checking initialization.
783 ///
784 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
785 ///
786 /// # Safety
787 ///
788 /// - Must be called from the R main thread.
789 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
790 #[inline]
791 pub unsafe fn try_unprotect_fast(x: SEXP) -> bool {
792 Self::with_state(|s| {
793 debug_assert!(s.initialized, "try_unprotect_fast called before init");
794 unsafe { s.inner.try_unprotect(x) }
795 })
796 }
797
798 /// Check if a SEXP is protected.
799 #[inline]
800 pub fn is_protected(x: SEXP) -> bool {
801 Self::with_state(|s| {
802 if !s.initialized {
803 return false;
804 }
805 s.inner.is_protected(x)
806 })
807 }
808
809 /// Get reference count.
810 #[inline]
811 pub fn ref_count(x: SEXP) -> usize {
812 Self::with_state(|s| {
813 if !s.initialized {
814 return 0;
815 }
816 s.inner.ref_count(x)
817 })
818 }
819
820 /// Number of protected SEXPs.
821 #[inline]
822 pub fn len() -> usize {
823 Self::with_state(|s| s.inner.len)
824 }
825
826 /// Check if empty.
827 #[inline]
828 pub fn is_empty() -> bool {
829 Self::len() == 0
830 }
831
832 /// Get capacity.
833 #[inline]
834 pub fn capacity() -> usize {
835 Self::with_state(|s| s.inner.capacity)
836 }
837
838 /// Clear all protections.
839 ///
840 /// # Safety
841 ///
842 /// Must be called from the R main thread.
843 pub unsafe fn clear() {
844 Self::with_state(|s| {
845 if s.initialized {
846 unsafe { s.inner.clear() };
847 }
848 });
849 }
850}
851
852// Tests are in tests/refcount_protect.rs (requires R runtime via miniextendr-engine)
853// endregion