miniextendr_api/refcount_protect.rs
1//! Reference-counted GC protection using a map + 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//! The module is built around two key abstractions:
10//!
11//! 1. **[`MapStorage`]** - Trait abstracting over map implementations (BTreeMap, HashMap)
12//! 2. **[`Arena`]** - Generic arena using RefCell for interior mutability
13//!
14//! For thread-local storage without RefCell overhead, use the `define_thread_local_arena!` macro.
15//!
16//! # How It Works
17//!
18//! ```text
19//! ┌─────────────────────────────────────────────────────────────────────┐
20//! │ Arena<M: MapStorage> │
21//! │ ┌─────────────────────────┐ ┌───────────────────────────────────┐│
22//! │ │ Map<usize, Entry> │ │ VECSXP (R_PreserveObject'd) ││
23//! │ │ ────────────────────── │ │ ───────────────────────────── ││
24//! │ │ sexp_a → {count:2, i:0}│◄──┤ [0]: sexp_a ││
25//! │ │ sexp_b → {count:1, i:1}│◄──┤ [1]: sexp_b ││
26//! │ │ sexp_c → {count:1, i:2}│◄──┤ [2]: sexp_c ││
27//! │ └─────────────────────────┘ │ [3]: <free> ││
28//! │ └───────────────────────────────────┘│
29//! └─────────────────────────────────────────────────────────────────────┘
30//! ```
31//!
32//! # Available Types
33//!
34//! | Type | Map | Storage | Use Case |
35//! |------|-----|---------|----------|
36//! | [`RefCountedArena`] | BTreeMap | RefCell | General purpose, ordered |
37//! | [`HashMapArena`] | HashMap | RefCell | Large collections |
38//! | [`ThreadLocalArena`] | BTreeMap | thread_local | Lowest overhead |
39//! | [`ThreadLocalHashArena`] | HashMap | thread_local | Large + low overhead |
40//!
41//! ## Fast Hash (feature-gated)
42//!
43//! With the `refcount-fast-hash` feature enabled, additional types become available:
44//!
45//! | Type | Map | Storage | Use Case |
46//! |------|-----|---------|----------|
47//! | [`FastHashMapArena`] | ahash HashMap | RefCell | Faster for large collections |
48//! | [`ThreadLocalFastHashArena`] | ahash HashMap | thread_local | Fastest for large + hot loops |
49//!
50//! These use ahash instead of SipHash for improved throughput. Not DOS-resistant,
51//! but suitable for local, non-hostile environments.
52
53use crate::ffi::{
54 R_PreserveObject, R_ReleaseObject, R_xlen_t, Rf_allocVector, Rf_protect, Rf_unprotect, SEXP,
55 SEXPTYPE, SexpExt,
56};
57use std::cell::RefCell;
58use std::collections::{BTreeMap, HashMap};
59use std::marker::PhantomData;
60use std::mem::MaybeUninit;
61use std::rc::Rc;
62
63// region: Entry type
64
65/// Entry in the reference count map.
66///
67/// This is an implementation detail exposed for generic type bounds.
68#[derive(Debug, Clone, Copy)]
69#[doc(hidden)]
70pub struct Entry {
71 /// Reference count (how many times this SEXP has been protected)
72 count: usize,
73 /// Index in the backing VECSXP
74 index: usize,
75}
76// endregion
77
78// region: MapStorage trait
79
80/// Trait abstracting over map implementations for arena storage.
81///
82/// This allows [`Arena`] to be generic over the underlying map type,
83/// supporting both `BTreeMap` and `HashMap`.
84pub trait MapStorage: Default {
85 /// Get an entry by key.
86 fn get(&self, key: &usize) -> Option<&Entry>;
87
88 /// Get a mutable entry by key.
89 fn get_mut(&mut self, key: &usize) -> Option<&mut Entry>;
90
91 /// Insert an entry, returning the old value if present.
92 fn insert(&mut self, key: usize, entry: Entry) -> Option<Entry>;
93
94 /// Remove an entry by key.
95 fn remove(&mut self, key: &usize) -> Option<Entry>;
96
97 /// Check if a key exists.
98 fn contains_key(&self, key: &usize) -> bool;
99
100 /// Iterate over all entries.
101 fn for_each_entry<F: FnMut(&Entry)>(&self, f: F);
102
103 /// Clear all entries.
104 fn clear(&mut self);
105
106 /// Reserve capacity for additional entries.
107 ///
108 /// This is a no-op for ordered maps (BTreeMap) but can improve performance
109 /// for hash maps by avoiding rehashing during bulk inserts.
110 #[inline]
111 fn reserve(&mut self, _additional: usize) {
112 // Default no-op for maps that don't support reservation
113 }
114
115 /// Decrement the count for a key and remove if zero.
116 ///
117 /// Returns `Some(true)` if entry was found and removed,
118 /// `Some(false)` if entry was found but count > 0 after decrement,
119 /// `None` if entry was not found.
120 ///
121 /// This uses entry API when available for single-lookup performance.
122 fn decrement_and_maybe_remove(&mut self, key: &usize) -> Option<(bool, usize)> {
123 if let Some(entry) = self.get_mut(key) {
124 entry.count -= 1;
125 if entry.count == 0 {
126 let index = entry.index;
127 self.remove(key);
128 Some((true, index))
129 } else {
130 Some((false, entry.index))
131 }
132 } else {
133 None
134 }
135 }
136}
137
138impl MapStorage for BTreeMap<usize, Entry> {
139 #[inline]
140 fn get(&self, key: &usize) -> Option<&Entry> {
141 BTreeMap::get(self, key)
142 }
143
144 #[inline]
145 fn get_mut(&mut self, key: &usize) -> Option<&mut Entry> {
146 BTreeMap::get_mut(self, key)
147 }
148
149 #[inline]
150 fn insert(&mut self, key: usize, entry: Entry) -> Option<Entry> {
151 BTreeMap::insert(self, key, entry)
152 }
153
154 #[inline]
155 fn remove(&mut self, key: &usize) -> Option<Entry> {
156 BTreeMap::remove(self, key)
157 }
158
159 #[inline]
160 fn contains_key(&self, key: &usize) -> bool {
161 BTreeMap::contains_key(self, key)
162 }
163
164 #[inline]
165 fn for_each_entry<F: FnMut(&Entry)>(&self, mut f: F) {
166 for entry in self.values() {
167 f(entry);
168 }
169 }
170
171 #[inline]
172 fn clear(&mut self) {
173 BTreeMap::clear(self);
174 }
175}
176
177/// Implement [`MapStorage`] for a HashMap-like type.
178///
179/// Both `HashMap<usize, Entry>` and `FastHashMap` (ahash) share the same API
180/// including the entry API for single-lookup decrement. This macro avoids
181/// duplicating the ~60-line impl for each hasher type.
182macro_rules! impl_hashmap_map_storage {
183 ($ty:ty) => {
184 impl MapStorage for $ty {
185 #[inline]
186 fn get(&self, key: &usize) -> Option<&Entry> {
187 std::collections::HashMap::get(self, key)
188 }
189
190 #[inline]
191 fn get_mut(&mut self, key: &usize) -> Option<&mut Entry> {
192 std::collections::HashMap::get_mut(self, key)
193 }
194
195 #[inline]
196 fn insert(&mut self, key: usize, entry: Entry) -> Option<Entry> {
197 std::collections::HashMap::insert(self, key, entry)
198 }
199
200 #[inline]
201 fn remove(&mut self, key: &usize) -> Option<Entry> {
202 std::collections::HashMap::remove(self, key)
203 }
204
205 #[inline]
206 fn contains_key(&self, key: &usize) -> bool {
207 std::collections::HashMap::contains_key(self, key)
208 }
209
210 #[inline]
211 fn for_each_entry<F: FnMut(&Entry)>(&self, mut f: F) {
212 for entry in self.values() {
213 f(entry);
214 }
215 }
216
217 #[inline]
218 fn clear(&mut self) {
219 std::collections::HashMap::clear(self);
220 }
221
222 #[inline]
223 fn reserve(&mut self, additional: usize) {
224 std::collections::HashMap::reserve(self, additional);
225 }
226
227 /// Entry-based decrement for single-lookup performance.
228 #[inline]
229 fn decrement_and_maybe_remove(&mut self, key: &usize) -> Option<(bool, usize)> {
230 use std::collections::hash_map::Entry as HashEntry;
231
232 match self.entry(*key) {
233 HashEntry::Occupied(mut occupied) => {
234 let entry = occupied.get_mut();
235 entry.count -= 1;
236 if entry.count == 0 {
237 let index = entry.index;
238 occupied.remove();
239 Some((true, index))
240 } else {
241 Some((false, entry.index))
242 }
243 }
244 HashEntry::Vacant(_) => None,
245 }
246 }
247 }
248 };
249}
250
251impl_hashmap_map_storage!(HashMap<usize, Entry>);
252// endregion
253
254// region: Core arena state (shared between RefCell and thread_local variants)
255
256/// Core arena state without interior mutability.
257///
258/// This is used internally by both [`Arena`] (with RefCell) and
259/// thread-local arenas (with UnsafeCell).
260#[doc(hidden)]
261pub struct ArenaState<M> {
262 /// Map from SEXP pointer to entry
263 pub map: MaybeUninit<M>,
264 /// Backing VECSXP (preserved via R_PreserveObject)
265 pub backing: SEXP,
266 /// Current capacity
267 pub capacity: usize,
268 /// Number of active entries
269 pub len: usize,
270 /// Free list for slot reuse
271 pub free_list: Vec<usize>,
272}
273
274impl<M: MapStorage> ArenaState<M> {
275 /// Initial capacity for the backing VECSXP.
276 ///
277 /// This is suitable for light usage (a handful of protected values).
278 /// For ppsize-scale workloads (hundreds or thousands of protected values),
279 /// use [`Arena::with_capacity`] or [`init_with_capacity`](ThreadLocalState::init_with_capacity)
280 /// to avoid repeated backing VECSXP growth and map rehashing.
281 pub const INITIAL_CAPACITY: usize = 16;
282
283 /// Maximum capacity: the backing VECSXP is indexed by `R_xlen_t` (isize),
284 /// so the capacity must fit in a non-negative `R_xlen_t`.
285 const MAX_CAPACITY: usize = R_xlen_t::MAX as usize;
286
287 /// Convert a `usize` capacity to `R_xlen_t`, panicking on overflow.
288 #[inline]
289 fn capacity_as_r_xlen(cap: usize) -> R_xlen_t {
290 R_xlen_t::try_from(cap).unwrap_or_else(|_| {
291 panic!(
292 "arena capacity {} exceeds R_xlen_t::MAX ({})",
293 cap,
294 R_xlen_t::MAX
295 )
296 })
297 }
298
299 /// Create uninitialized state (for thread_local).
300 pub const fn uninit() -> Self {
301 Self {
302 map: MaybeUninit::uninit(),
303 backing: SEXP(std::ptr::null_mut()),
304 capacity: 0,
305 len: 0,
306 free_list: Vec::new(),
307 }
308 }
309
310 /// Initialize the state.
311 ///
312 /// # Safety
313 ///
314 /// Must be called exactly once before using the state.
315 pub unsafe fn init(&mut self, capacity: usize) {
316 let capacity = capacity.max(1);
317 assert!(
318 capacity <= Self::MAX_CAPACITY,
319 "arena capacity {} exceeds R_xlen_t::MAX ({})",
320 capacity,
321 R_xlen_t::MAX
322 );
323 unsafe {
324 let r_cap = Self::capacity_as_r_xlen(capacity);
325 let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
326 R_PreserveObject(backing);
327 Rf_unprotect(1);
328
329 let mut map = M::default();
330 map.reserve(capacity);
331 self.map.write(map);
332 self.backing = backing;
333 self.capacity = capacity;
334 self.len = 0;
335 self.free_list = Vec::with_capacity(capacity);
336 }
337 }
338
339 /// Create initialized state.
340 unsafe fn new(capacity: usize) -> Self {
341 let capacity = capacity.max(1);
342 let mut map = M::default();
343 map.reserve(capacity);
344 let mut state = Self {
345 map: MaybeUninit::new(map),
346 backing: SEXP(std::ptr::null_mut()),
347 capacity: 0,
348 len: 0,
349 free_list: Vec::with_capacity(capacity),
350 };
351 unsafe { state.init_backing(capacity) };
352 state
353 }
354
355 /// Initialize just the backing (map already initialized).
356 unsafe fn init_backing(&mut self, capacity: usize) {
357 let capacity = capacity.max(1);
358 assert!(
359 capacity <= Self::MAX_CAPACITY,
360 "arena capacity {} exceeds R_xlen_t::MAX ({})",
361 capacity,
362 R_xlen_t::MAX
363 );
364 unsafe {
365 let r_cap = Self::capacity_as_r_xlen(capacity);
366 let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
367 R_PreserveObject(backing);
368 Rf_unprotect(1);
369
370 self.backing = backing;
371 self.capacity = capacity;
372 }
373 }
374
375 /// Get a reference to the map.
376 #[inline]
377 fn map(&self) -> &M {
378 // SAFETY: Map is initialized before any access
379 unsafe { self.map.assume_init_ref() }
380 }
381
382 /// Get a mutable reference to the map.
383 #[inline]
384 fn map_mut(&mut self) -> &mut M {
385 // SAFETY: Map is initialized before any access
386 unsafe { self.map.assume_init_mut() }
387 }
388
389 /// Protect a SEXP from garbage collection.
390 ///
391 /// # Safety
392 ///
393 /// Must be called from the R main thread. The SEXP must be valid.
394 #[inline]
395 pub unsafe fn protect(&mut self, x: SEXP) -> SEXP {
396 if x.is_nil() {
397 return x;
398 }
399
400 let key = x.0 as usize;
401
402 if let Some(entry) = self.map_mut().get_mut(&key) {
403 entry.count += 1;
404 } else {
405 let index = self.allocate_slot();
406 self.backing.set_vector_elt(index as R_xlen_t, x);
407 self.map_mut().insert(key, Entry { count: 1, index });
408 self.len += 1;
409 }
410
411 x
412 }
413
414 /// Unprotect a SEXP, allowing garbage collection when refcount reaches zero.
415 ///
416 /// # Safety
417 ///
418 /// Must be called from the R main thread. The SEXP must have been
419 /// previously protected by this arena.
420 #[inline]
421 pub unsafe fn unprotect(&mut self, x: SEXP) {
422 if x.is_nil() {
423 return;
424 }
425
426 let key = x.0 as usize;
427
428 // Use entry-based single-lookup for HashMap, double-lookup for BTreeMap
429 match self.map_mut().decrement_and_maybe_remove(&key) {
430 Some((true, index)) => {
431 // Entry was removed (count reached 0)
432 self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
433 self.free_list.push(index);
434 self.len -= 1;
435 }
436 Some((false, _)) => {
437 // Entry still exists (count > 0)
438 }
439 None => {
440 panic!("unprotect called on SEXP not protected by this arena");
441 }
442 }
443 }
444
445 /// Try to unprotect a SEXP. Returns false if not protected by this arena.
446 ///
447 /// # Safety
448 ///
449 /// Must be called from the R main thread.
450 #[inline]
451 pub unsafe fn try_unprotect(&mut self, x: SEXP) -> bool {
452 if x.is_nil() {
453 return false;
454 }
455
456 let key = x.0 as usize;
457
458 // Use entry-based single-lookup for HashMap, double-lookup for BTreeMap
459 match self.map_mut().decrement_and_maybe_remove(&key) {
460 Some((true, index)) => {
461 // Entry was removed (count reached 0)
462 self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
463 self.free_list.push(index);
464 self.len -= 1;
465 true
466 }
467 Some((false, _)) => {
468 // Entry still exists (count > 0)
469 true
470 }
471 None => false,
472 }
473 }
474
475 #[inline]
476 /// Returns true if this arena currently protects `x`.
477 pub fn is_protected(&self, x: SEXP) -> bool {
478 if x.is_nil() {
479 return false;
480 }
481 let key = x.0 as usize;
482 self.map().contains_key(&key)
483 }
484
485 #[inline]
486 /// Returns the current reference count for `x` in this arena.
487 ///
488 /// Returns 0 if `x` is not protected or is `SEXP::nil()`.
489 pub fn ref_count(&self, x: SEXP) -> usize {
490 if x.is_nil() {
491 return 0;
492 }
493 let key = x.0 as usize;
494 self.map().get(&key).map(|e| e.count).unwrap_or(0)
495 }
496
497 fn allocate_slot(&mut self) -> usize {
498 if let Some(index) = self.free_list.pop() {
499 return index;
500 }
501
502 if self.len >= self.capacity {
503 unsafe { self.grow() };
504 }
505
506 self.len
507 }
508
509 unsafe fn grow(&mut self) {
510 let old_capacity = self.capacity;
511 let new_capacity = old_capacity
512 .checked_mul(2)
513 .expect("arena capacity overflow during growth");
514 assert!(
515 new_capacity <= Self::MAX_CAPACITY,
516 "arena capacity {} would exceed R_xlen_t::MAX ({}) after growth",
517 new_capacity,
518 R_xlen_t::MAX
519 );
520 let old_backing = self.backing;
521
522 unsafe {
523 let r_new_cap = Self::capacity_as_r_xlen(new_capacity);
524 let new_backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_new_cap));
525 R_PreserveObject(new_backing);
526
527 for i in 0..old_capacity {
528 let r_i = Self::capacity_as_r_xlen(i);
529 let elt = old_backing.vector_elt(r_i);
530 new_backing.set_vector_elt(r_i, elt);
531 }
532
533 R_ReleaseObject(old_backing);
534 Rf_unprotect(1);
535
536 self.backing = new_backing;
537 self.capacity = new_capacity;
538 }
539 }
540
541 /// Clear all protected values from the arena.
542 ///
543 /// # Safety
544 ///
545 /// Must be called from the R main thread.
546 pub unsafe fn clear(&mut self) {
547 self.map().for_each_entry(|entry| {
548 self.backing
549 .set_vector_elt(entry.index as R_xlen_t, SEXP::nil());
550 });
551 self.map_mut().clear();
552 self.free_list.clear();
553 self.len = 0;
554 }
555
556 unsafe fn release_backing(&mut self) {
557 if !self.backing.0.is_null() {
558 unsafe { R_ReleaseObject(self.backing) };
559 self.backing = SEXP(std::ptr::null_mut());
560 }
561 }
562}
563// endregion
564
565// region: Arena<M> - RefCell-based generic arena
566
567/// Enforces `!Send + !Sync` (R API is not thread-safe).
568type NoSendSync = PhantomData<Rc<()>>;
569
570/// A reference-counted arena for GC protection, generic over map type.
571///
572/// This provides an alternative to R's PROTECT stack that:
573/// - Uses reference counting for each SEXP
574/// - Allows releasing protections in any order
575/// - Has no stack size limit (uses heap allocation)
576///
577/// # Type Aliases
578///
579/// - [`RefCountedArena`] = `Arena<BTreeMap<...>>` (ordered, good for ref counting)
580/// - [`HashMapArena`] = `Arena<HashMap<...>>` (faster for large collections)
581pub struct Arena<M: MapStorage> {
582 state: RefCell<ArenaState<M>>,
583 _nosend: NoSendSync,
584}
585
586impl<M: MapStorage> Arena<M> {
587 /// Create a new arena with default capacity (16 slots).
588 ///
589 /// For workloads protecting many distinct SEXPs (e.g., ppsize-scale loops),
590 /// prefer [`with_capacity`](Self::with_capacity) to avoid backing VECSXP
591 /// growth and map rehashing during operation.
592 ///
593 /// # Safety
594 ///
595 /// Must be called from the R main thread.
596 pub unsafe fn new() -> Self {
597 unsafe { Self::with_capacity(ArenaState::<M>::INITIAL_CAPACITY) }
598 }
599
600 /// Create a new arena with specific initial capacity.
601 ///
602 /// Pre-sizing the arena avoids growth of the backing VECSXP and rehashing
603 /// of the internal map. Use this when the expected number of distinct
604 /// protected values is known or can be estimated.
605 ///
606 /// # Safety
607 ///
608 /// Must be called from the R main thread.
609 pub unsafe fn with_capacity(capacity: usize) -> Self {
610 Self {
611 state: RefCell::new(unsafe { ArenaState::new(capacity) }),
612 _nosend: PhantomData,
613 }
614 }
615
616 /// Protect a SEXP, incrementing its reference count.
617 ///
618 /// # Safety
619 ///
620 /// Must be called from the R main thread.
621 #[inline]
622 pub unsafe fn protect(&self, x: SEXP) -> SEXP {
623 unsafe { self.state.borrow_mut().protect(x) }
624 }
625
626 /// Unprotect a SEXP, decrementing its reference count.
627 ///
628 /// # Safety
629 ///
630 /// Must be called from the R main thread.
631 ///
632 /// # Panics
633 ///
634 /// Panics if `x` was not protected by this arena.
635 #[inline]
636 pub unsafe fn unprotect(&self, x: SEXP) {
637 unsafe { self.state.borrow_mut().unprotect(x) };
638 }
639
640 /// Try to unprotect a SEXP, returning `true` if it was protected.
641 ///
642 /// # Safety
643 ///
644 /// Must be called from the R main thread.
645 #[inline]
646 pub unsafe fn try_unprotect(&self, x: SEXP) -> bool {
647 unsafe { self.state.borrow_mut().try_unprotect(x) }
648 }
649
650 /// Check if a SEXP is currently protected by this arena.
651 #[inline]
652 pub fn is_protected(&self, x: SEXP) -> bool {
653 self.state.borrow().is_protected(x)
654 }
655
656 /// Get the reference count for a SEXP (0 if not protected).
657 #[inline]
658 pub fn ref_count(&self, x: SEXP) -> usize {
659 self.state.borrow().ref_count(x)
660 }
661
662 /// Get the number of distinct SEXPs currently protected.
663 #[inline]
664 pub fn len(&self) -> usize {
665 self.state.borrow().len
666 }
667
668 /// Check if the arena is empty.
669 #[inline]
670 pub fn is_empty(&self) -> bool {
671 self.state.borrow().len == 0
672 }
673
674 /// Get the current capacity.
675 #[inline]
676 pub fn capacity(&self) -> usize {
677 self.state.borrow().capacity
678 }
679
680 /// Clear all protections.
681 ///
682 /// # Safety
683 ///
684 /// Must be called from the R main thread.
685 pub unsafe fn clear(&self) {
686 unsafe { self.state.borrow_mut().clear() };
687 }
688
689 /// Protect a SEXP and return an RAII guard.
690 ///
691 /// # Safety
692 ///
693 /// Must be called from the R main thread.
694 #[inline]
695 pub unsafe fn guard(&self, x: SEXP) -> ArenaGuard<'_, M> {
696 unsafe { ArenaGuard::new(self, x) }
697 }
698}
699
700impl<M: MapStorage> Drop for Arena<M> {
701 fn drop(&mut self) {
702 let state = self.state.get_mut();
703 // Drop the map first (always initialized for Arena<M>, which uses ArenaState::new()).
704 // SAFETY: Arena<M> always constructs via ArenaState::new() which initializes the map.
705 unsafe { state.map.assume_init_drop() };
706 unsafe { state.release_backing() };
707 }
708}
709
710impl<M: MapStorage> Default for Arena<M> {
711 fn default() -> Self {
712 unsafe { Self::new() }
713 }
714}
715// endregion
716
717// region: Type aliases for common arena types
718
719/// BTreeMap-based arena (default, good for reference counting).
720pub type RefCountedArena = Arena<BTreeMap<usize, Entry>>;
721
722/// HashMap-based arena (faster for large collections).
723pub type HashMapArena = Arena<HashMap<usize, Entry>>;
724// endregion
725
726// region: Fast hash types (feature-gated)
727
728/// HashMap with ahash for faster hashing (not DOS-resistant).
729///
730/// Uses ahash instead of SipHash for improved throughput. Not DOS-resistant,
731/// suitable for local, non-hostile environments.
732#[cfg(feature = "refcount-fast-hash")]
733pub type FastHashMap = std::collections::HashMap<usize, Entry, ahash::RandomState>;
734
735#[cfg(feature = "refcount-fast-hash")]
736impl_hashmap_map_storage!(FastHashMap);
737
738/// Fast hash arena using ahash (requires `refcount-fast-hash` feature).
739///
740/// Uses ahash instead of SipHash for improved throughput on large collections.
741/// Not DOS-resistant, suitable for local, non-hostile environments.
742#[cfg(feature = "refcount-fast-hash")]
743pub type FastHashMapArena = Arena<FastHashMap>;
744// endregion
745
746// region: RAII Guard
747
748/// An RAII guard that unprotects a SEXP when dropped.
749pub struct ArenaGuard<'a, M: MapStorage> {
750 arena: &'a Arena<M>,
751 sexp: SEXP,
752}
753
754impl<'a, M: MapStorage> ArenaGuard<'a, M> {
755 /// Create a new guard that protects the SEXP and unprotects on drop.
756 ///
757 /// # Safety
758 ///
759 /// Must be called from the R main thread. The SEXP must be valid.
760 #[inline]
761 pub unsafe fn new(arena: &'a Arena<M>, sexp: SEXP) -> Self {
762 unsafe { arena.protect(sexp) };
763 Self { arena, sexp }
764 }
765
766 #[inline]
767 /// Returns the protected SEXP.
768 pub fn get(&self) -> SEXP {
769 self.sexp
770 }
771}
772
773impl<M: MapStorage> Drop for ArenaGuard<'_, M> {
774 fn drop(&mut self) {
775 unsafe { self.arena.unprotect(self.sexp) };
776 }
777}
778
779impl<M: MapStorage> std::ops::Deref for ArenaGuard<'_, M> {
780 type Target = SEXP;
781
782 #[inline]
783 fn deref(&self) -> &Self::Target {
784 &self.sexp
785 }
786}
787
788/// Legacy type alias for backwards compatibility.
789pub type RefCountedGuard<'a> = ArenaGuard<'a, BTreeMap<usize, Entry>>;
790// endregion
791
792// region: Thread-local arena trait + macro
793
794/// Trait providing default implementations for all thread-local arena methods.
795///
796/// Implementors only need to provide [`with_state`](Self::with_state) to access
797/// the thread-local state; all 14 arena methods are provided as defaults.
798///
799/// The `define_thread_local_arena!` macro generates both the struct and the
800/// `ThreadLocalArenaOps` impl, so this trait is an implementation detail.
801/// Import it when calling methods on thread-local arena types:
802///
803/// ```ignore
804/// use miniextendr_api::refcount_protect::{ThreadLocalArena, ThreadLocalArenaOps};
805/// unsafe { ThreadLocalArena::protect(x) };
806/// ```
807pub trait ThreadLocalArenaOps {
808 /// The map storage type used by this arena.
809 type Map: MapStorage;
810
811 /// Access the thread-local state.
812 ///
813 /// Implementors route through `thread_local!` + `UnsafeCell`.
814 fn with_state<R, F: FnOnce(&mut ThreadLocalState<Self::Map>) -> R>(f: F) -> R;
815
816 /// Initialize the arena with default capacity (called automatically on first use).
817 ///
818 /// # Safety
819 ///
820 /// Must be called from the R main thread.
821 unsafe fn init() {
822 Self::with_state(|s| {
823 if !s.initialized {
824 unsafe { s.init() };
825 }
826 });
827 }
828
829 /// Initialize the arena with specific capacity.
830 ///
831 /// Use this when you know the expected number of distinct protected values
832 /// to avoid backing VECSXP growth and map rehashing during operation.
833 ///
834 /// If already initialized, this is a no-op.
835 ///
836 /// # Safety
837 ///
838 /// Must be called from the R main thread.
839 unsafe fn init_with_capacity(capacity: usize) {
840 Self::with_state(|s| {
841 if !s.initialized {
842 unsafe { s.init_with_capacity(capacity) };
843 }
844 });
845 }
846
847 /// Protect a SEXP, incrementing its reference count.
848 ///
849 /// # Safety
850 ///
851 /// Must be called from the R main thread.
852 #[inline]
853 unsafe fn protect(x: SEXP) -> SEXP {
854 Self::with_state(|s| {
855 if !s.initialized {
856 unsafe { s.init() };
857 }
858 unsafe { s.inner.protect(x) }
859 })
860 }
861
862 /// Unprotect a SEXP.
863 ///
864 /// # Safety
865 ///
866 /// Must be called from the R main thread.
867 #[inline]
868 unsafe fn unprotect(x: SEXP) {
869 Self::with_state(|s| {
870 // If the arena was never initialized, no SEXP could have been
871 // protected by it, so there is nothing to unprotect.
872 if !s.initialized {
873 return;
874 }
875 unsafe { s.inner.unprotect(x) };
876 });
877 }
878
879 /// Try to unprotect a SEXP.
880 ///
881 /// # Safety
882 ///
883 /// Must be called from the R main thread.
884 #[inline]
885 unsafe fn try_unprotect(x: SEXP) -> bool {
886 Self::with_state(|s| {
887 // If the arena was never initialized, no SEXP could have been
888 // protected by it, so return false.
889 if !s.initialized {
890 return false;
891 }
892 unsafe { s.inner.try_unprotect(x) }
893 })
894 }
895
896 /// Protect without checking initialization.
897 ///
898 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
899 ///
900 /// # Safety
901 ///
902 /// - Must be called from the R main thread.
903 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
904 #[inline]
905 unsafe fn protect_fast(x: SEXP) -> SEXP {
906 Self::with_state(|s| {
907 debug_assert!(s.initialized, "protect_fast called before init");
908 unsafe { s.inner.protect(x) }
909 })
910 }
911
912 /// Unprotect without checking initialization.
913 ///
914 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
915 ///
916 /// # Safety
917 ///
918 /// - Must be called from the R main thread.
919 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
920 #[inline]
921 unsafe fn unprotect_fast(x: SEXP) {
922 Self::with_state(|s| {
923 debug_assert!(s.initialized, "unprotect_fast called before init");
924 unsafe { s.inner.unprotect(x) };
925 });
926 }
927
928 /// Try to unprotect without checking initialization.
929 ///
930 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
931 ///
932 /// # Safety
933 ///
934 /// - Must be called from the R main thread.
935 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
936 #[inline]
937 unsafe fn try_unprotect_fast(x: SEXP) -> bool {
938 Self::with_state(|s| {
939 debug_assert!(s.initialized, "try_unprotect_fast called before init");
940 unsafe { s.inner.try_unprotect(x) }
941 })
942 }
943
944 /// Check if a SEXP is protected.
945 #[inline]
946 fn is_protected(x: SEXP) -> bool {
947 Self::with_state(|s| {
948 if !s.initialized {
949 return false;
950 }
951 s.inner.is_protected(x)
952 })
953 }
954
955 /// Get reference count.
956 #[inline]
957 fn ref_count(x: SEXP) -> usize {
958 Self::with_state(|s| {
959 if !s.initialized {
960 return 0;
961 }
962 s.inner.ref_count(x)
963 })
964 }
965
966 /// Number of protected SEXPs.
967 #[inline]
968 fn len() -> usize {
969 Self::with_state(|s| s.inner.len)
970 }
971
972 /// Check if empty.
973 #[inline]
974 fn is_empty() -> bool {
975 Self::len() == 0
976 }
977
978 /// Get capacity.
979 #[inline]
980 fn capacity() -> usize {
981 Self::with_state(|s| s.inner.capacity)
982 }
983
984 /// Clear all protections.
985 ///
986 /// # Safety
987 ///
988 /// Must be called from the R main thread.
989 unsafe fn clear() {
990 Self::with_state(|s| {
991 if s.initialized {
992 unsafe { s.inner.clear() };
993 }
994 });
995 }
996}
997
998/// Macro to define a thread-local arena with a specific map type.
999///
1000/// This creates a zero-sized struct implementing [`ThreadLocalArenaOps`],
1001/// providing all arena methods via the trait's default implementations.
1002///
1003/// # Example
1004///
1005/// ```ignore
1006/// define_thread_local_arena!(
1007/// /// My custom thread-local arena.
1008/// pub MyArena,
1009/// BTreeMap<usize, Entry>,
1010/// MY_ARENA_STATE
1011/// );
1012/// ```
1013#[macro_export]
1014macro_rules! define_thread_local_arena {
1015 (
1016 $(#[$meta:meta])*
1017 $vis:vis $name:ident,
1018 $map:ty,
1019 $state_name:ident
1020 ) => {
1021 thread_local! {
1022 static $state_name: std::cell::UnsafeCell<$crate::refcount_protect::ThreadLocalState<$map>> =
1023 const { std::cell::UnsafeCell::new($crate::refcount_protect::ThreadLocalState::uninit()) };
1024 }
1025
1026 $(#[$meta])*
1027 $vis struct $name;
1028
1029 impl $crate::refcount_protect::ThreadLocalArenaOps for $name {
1030 type Map = $map;
1031
1032 fn with_state<R, F: FnOnce(&mut $crate::refcount_protect::ThreadLocalState<$map>) -> R>(f: F) -> R {
1033 $state_name.with(|cell| f(unsafe { &mut *cell.get() }))
1034 }
1035 }
1036 };
1037}
1038
1039/// State wrapper for thread-local arenas (used by macro).
1040#[doc(hidden)]
1041pub struct ThreadLocalState<M: MapStorage> {
1042 pub inner: ArenaState<M>,
1043 pub initialized: bool,
1044}
1045
1046impl<M: MapStorage> ThreadLocalState<M> {
1047 /// Create an uninitialized thread-local arena state.
1048 ///
1049 /// Call `init` or `init_with_capacity` before use.
1050 pub const fn uninit() -> Self {
1051 Self {
1052 inner: ArenaState::uninit(),
1053 initialized: false,
1054 }
1055 }
1056
1057 /// Initialize with default capacity (16 slots).
1058 ///
1059 /// For ppsize-scale workloads, prefer [`init_with_capacity`](Self::init_with_capacity)
1060 /// to avoid backing VECSXP growth and map rehashing during operation.
1061 ///
1062 /// # Safety
1063 ///
1064 /// Must be called from the R main thread. Must only be called once.
1065 pub unsafe fn init(&mut self) {
1066 unsafe { self.inner.init(ArenaState::<M>::INITIAL_CAPACITY) };
1067 self.initialized = true;
1068 }
1069
1070 /// Initialize with specific capacity.
1071 ///
1072 /// Pre-sizing avoids growth of the backing VECSXP and rehashing of the
1073 /// internal map. Use this when the expected number of distinct protected
1074 /// values is known or can be estimated (e.g., the length of an input vector).
1075 ///
1076 /// # Safety
1077 ///
1078 /// Must be called from the R main thread. Must only be called once.
1079 pub unsafe fn init_with_capacity(&mut self, capacity: usize) {
1080 unsafe { self.inner.init(capacity) };
1081 self.initialized = true;
1082 }
1083}
1084
1085impl<M: MapStorage> Drop for ThreadLocalState<M> {
1086 fn drop(&mut self) {
1087 if self.initialized {
1088 // SAFETY: The map was initialized in init() or init_with_capacity().
1089 // We must manually drop it because MaybeUninit does not run Drop.
1090 unsafe { self.inner.map.assume_init_drop() };
1091 }
1092 // R backing is released separately via release_backing() if needed.
1093 // Thread-local destructors may run after R has shut down, so we do NOT
1094 // call R_ReleaseObject here — the R runtime owns the backing VECSXP
1095 // lifetime via R_PreserveObject.
1096 }
1097}
1098// endregion
1099
1100// region: Built-in thread-local arenas
1101
1102define_thread_local_arena!(
1103 /// Thread-local BTreeMap-based arena.
1104 ///
1105 /// This provides the lowest overhead for protection operations by
1106 /// eliminating RefCell borrow checking.
1107 pub ThreadLocalArena,
1108 BTreeMap<usize, Entry>,
1109 THREAD_LOCAL_BTREE_STATE
1110);
1111
1112define_thread_local_arena!(
1113 /// Thread-local HashMap-based arena.
1114 ///
1115 /// Combines HashMap's performance for large collections with
1116 /// thread-local storage's low overhead.
1117 pub ThreadLocalHashArena,
1118 HashMap<usize, Entry>,
1119 THREAD_LOCAL_HASH_STATE
1120);
1121// endregion
1122
1123// region: Fast hash thread-local arena (feature-gated)
1124
1125#[cfg(feature = "refcount-fast-hash")]
1126define_thread_local_arena!(
1127 /// Thread-local fast hash arena using ahash.
1128 ///
1129 /// Combines ahash's faster hashing with thread-local storage's low overhead.
1130 /// Ideal for hot loops protecting many distinct values.
1131 ///
1132 /// Not DOS-resistant, suitable for local, non-hostile environments.
1133 ///
1134 /// Requires the `refcount-fast-hash` feature.
1135 pub ThreadLocalFastHashArena,
1136 FastHashMap,
1137 THREAD_LOCAL_FAST_HASH_STATE
1138);
1139
1140// Tests are in tests/refcount_protect.rs (requires R runtime via miniextendr-engine)
1141// endregion