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 /// Monotonic write cursor: next index to hand out on the fresh-slot path.
273 /// Distinct from `len` (live count) — after release cycles `len` can be
274 /// lower than the highest index ever handed out, so using `len` as the
275 /// cursor can return an index that is already on the free-list.
276 pub next_slot: usize,
277}
278
279impl<M: MapStorage> ArenaState<M> {
280 /// Initial capacity for the backing VECSXP.
281 ///
282 /// This is suitable for light usage (a handful of protected values).
283 /// For ppsize-scale workloads (hundreds or thousands of protected values),
284 /// use [`Arena::with_capacity`] or [`init_with_capacity`](ThreadLocalState::init_with_capacity)
285 /// to avoid repeated backing VECSXP growth and map rehashing.
286 pub const INITIAL_CAPACITY: usize = 16;
287
288 /// Maximum capacity: the backing VECSXP is indexed by `R_xlen_t` (isize),
289 /// so the capacity must fit in a non-negative `R_xlen_t`.
290 const MAX_CAPACITY: usize = R_xlen_t::MAX as usize;
291
292 /// Convert a `usize` capacity to `R_xlen_t`, panicking on overflow.
293 #[inline]
294 fn capacity_as_r_xlen(cap: usize) -> R_xlen_t {
295 R_xlen_t::try_from(cap).unwrap_or_else(|_| {
296 panic!(
297 "arena capacity {} exceeds R_xlen_t::MAX ({})",
298 cap,
299 R_xlen_t::MAX
300 )
301 })
302 }
303
304 /// Create uninitialized state (for thread_local).
305 pub const fn uninit() -> Self {
306 Self {
307 map: MaybeUninit::uninit(),
308 backing: SEXP(std::ptr::null_mut()),
309 capacity: 0,
310 len: 0,
311 free_list: Vec::new(),
312 next_slot: 0,
313 }
314 }
315
316 /// Initialize the state.
317 ///
318 /// # Safety
319 ///
320 /// Must be called exactly once before using the state.
321 pub unsafe fn init(&mut self, capacity: usize) {
322 let capacity = capacity.max(1);
323 assert!(
324 capacity <= Self::MAX_CAPACITY,
325 "arena capacity {} exceeds R_xlen_t::MAX ({})",
326 capacity,
327 R_xlen_t::MAX
328 );
329 unsafe {
330 let r_cap = Self::capacity_as_r_xlen(capacity);
331 let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
332 R_PreserveObject(backing);
333 Rf_unprotect(1);
334
335 let mut map = M::default();
336 map.reserve(capacity);
337 self.map.write(map);
338 self.backing = backing;
339 self.capacity = capacity;
340 self.len = 0;
341 self.next_slot = 0;
342 self.free_list = Vec::with_capacity(capacity);
343 }
344 }
345
346 /// Create initialized state.
347 unsafe fn new(capacity: usize) -> Self {
348 let capacity = capacity.max(1);
349 let mut map = M::default();
350 map.reserve(capacity);
351 let mut state = Self {
352 map: MaybeUninit::new(map),
353 backing: SEXP(std::ptr::null_mut()),
354 capacity: 0,
355 len: 0,
356 next_slot: 0,
357 free_list: Vec::with_capacity(capacity),
358 };
359 unsafe { state.init_backing(capacity) };
360 state
361 }
362
363 /// Initialize just the backing (map already initialized).
364 unsafe fn init_backing(&mut self, capacity: usize) {
365 let capacity = capacity.max(1);
366 assert!(
367 capacity <= Self::MAX_CAPACITY,
368 "arena capacity {} exceeds R_xlen_t::MAX ({})",
369 capacity,
370 R_xlen_t::MAX
371 );
372 unsafe {
373 let r_cap = Self::capacity_as_r_xlen(capacity);
374 let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
375 R_PreserveObject(backing);
376 Rf_unprotect(1);
377
378 self.backing = backing;
379 self.capacity = capacity;
380 }
381 }
382
383 /// Get a reference to the map.
384 #[inline]
385 fn map(&self) -> &M {
386 // SAFETY: Map is initialized before any access
387 unsafe { self.map.assume_init_ref() }
388 }
389
390 /// Get a mutable reference to the map.
391 #[inline]
392 fn map_mut(&mut self) -> &mut M {
393 // SAFETY: Map is initialized before any access
394 unsafe { self.map.assume_init_mut() }
395 }
396
397 /// Protect a SEXP from garbage collection.
398 ///
399 /// # Safety
400 ///
401 /// Must be called from the R main thread. The SEXP must be valid.
402 #[inline]
403 pub unsafe fn protect(&mut self, x: SEXP) -> SEXP {
404 if x.is_nil() {
405 return x;
406 }
407
408 let key = x.0 as usize;
409
410 if let Some(entry) = self.map_mut().get_mut(&key) {
411 entry.count += 1;
412 } else {
413 let index = self.allocate_slot();
414 self.backing.set_vector_elt(index as R_xlen_t, x);
415 self.map_mut().insert(key, Entry { count: 1, index });
416 self.len += 1;
417 }
418
419 x
420 }
421
422 /// Unprotect a SEXP, allowing garbage collection when refcount reaches zero.
423 ///
424 /// # Safety
425 ///
426 /// Must be called from the R main thread. The SEXP must have been
427 /// previously protected by this arena.
428 #[inline]
429 pub unsafe fn unprotect(&mut self, x: SEXP) {
430 if x.is_nil() {
431 return;
432 }
433
434 let key = x.0 as usize;
435
436 // Use entry-based single-lookup for HashMap, double-lookup for BTreeMap
437 match self.map_mut().decrement_and_maybe_remove(&key) {
438 Some((true, index)) => {
439 // Entry was removed (count reached 0)
440 self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
441 self.free_list.push(index);
442 self.len -= 1;
443 }
444 Some((false, _)) => {
445 // Entry still exists (count > 0)
446 }
447 None => {
448 panic!("unprotect called on SEXP not protected by this arena");
449 }
450 }
451 }
452
453 /// Try to unprotect a SEXP. Returns false if not protected by this arena.
454 ///
455 /// # Safety
456 ///
457 /// Must be called from the R main thread.
458 #[inline]
459 pub unsafe fn try_unprotect(&mut self, x: SEXP) -> bool {
460 if x.is_nil() {
461 return false;
462 }
463
464 let key = x.0 as usize;
465
466 // Use entry-based single-lookup for HashMap, double-lookup for BTreeMap
467 match self.map_mut().decrement_and_maybe_remove(&key) {
468 Some((true, index)) => {
469 // Entry was removed (count reached 0)
470 self.backing.set_vector_elt(index as R_xlen_t, SEXP::nil());
471 self.free_list.push(index);
472 self.len -= 1;
473 true
474 }
475 Some((false, _)) => {
476 // Entry still exists (count > 0)
477 true
478 }
479 None => false,
480 }
481 }
482
483 #[inline]
484 /// Returns true if this arena currently protects `x`.
485 pub fn is_protected(&self, x: SEXP) -> bool {
486 if x.is_nil() {
487 return false;
488 }
489 let key = x.0 as usize;
490 self.map().contains_key(&key)
491 }
492
493 #[inline]
494 /// Returns the current reference count for `x` in this arena.
495 ///
496 /// Returns 0 if `x` is not protected or is `SEXP::nil()`.
497 pub fn ref_count(&self, x: SEXP) -> usize {
498 if x.is_nil() {
499 return 0;
500 }
501 let key = x.0 as usize;
502 self.map().get(&key).map(|e| e.count).unwrap_or(0)
503 }
504
505 fn allocate_slot(&mut self) -> usize {
506 if let Some(index) = self.free_list.pop() {
507 return index;
508 }
509
510 if self.next_slot >= self.capacity {
511 unsafe { self.grow() };
512 }
513
514 let idx = self.next_slot;
515 self.next_slot += 1;
516 idx
517 }
518
519 unsafe fn grow(&mut self) {
520 let old_capacity = self.capacity;
521 let new_capacity = old_capacity
522 .checked_mul(2)
523 .expect("arena capacity overflow during growth");
524 assert!(
525 new_capacity <= Self::MAX_CAPACITY,
526 "arena capacity {} would exceed R_xlen_t::MAX ({}) after growth",
527 new_capacity,
528 R_xlen_t::MAX
529 );
530 let old_backing = self.backing;
531
532 unsafe {
533 let r_new_cap = Self::capacity_as_r_xlen(new_capacity);
534 let new_backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_new_cap));
535 R_PreserveObject(new_backing);
536
537 for i in 0..old_capacity {
538 let r_i = Self::capacity_as_r_xlen(i);
539 let elt = old_backing.vector_elt(r_i);
540 new_backing.set_vector_elt(r_i, elt);
541 }
542
543 R_ReleaseObject(old_backing);
544 Rf_unprotect(1);
545
546 self.backing = new_backing;
547 self.capacity = new_capacity;
548 }
549 }
550
551 /// Clear all protected values from the arena.
552 ///
553 /// # Safety
554 ///
555 /// Must be called from the R main thread.
556 pub unsafe fn clear(&mut self) {
557 self.map().for_each_entry(|entry| {
558 self.backing
559 .set_vector_elt(entry.index as R_xlen_t, SEXP::nil());
560 });
561 self.map_mut().clear();
562 self.free_list.clear();
563 self.len = 0;
564 self.next_slot = 0;
565 }
566
567 unsafe fn release_backing(&mut self) {
568 if !self.backing.0.is_null() {
569 unsafe { R_ReleaseObject(self.backing) };
570 self.backing = SEXP(std::ptr::null_mut());
571 }
572 }
573}
574// endregion
575
576// region: Arena<M> - RefCell-based generic arena
577
578/// Enforces `!Send + !Sync` (R API is not thread-safe).
579type NoSendSync = PhantomData<Rc<()>>;
580
581/// A reference-counted arena for GC protection, generic over map type.
582///
583/// This provides an alternative to R's PROTECT stack that:
584/// - Uses reference counting for each SEXP
585/// - Allows releasing protections in any order
586/// - Has no stack size limit (uses heap allocation)
587///
588/// # Type Aliases
589///
590/// - [`RefCountedArena`] = `Arena<BTreeMap<...>>` (ordered, good for ref counting)
591/// - [`HashMapArena`] = `Arena<HashMap<...>>` (faster for large collections)
592pub struct Arena<M: MapStorage> {
593 state: RefCell<ArenaState<M>>,
594 _nosend: NoSendSync,
595}
596
597impl<M: MapStorage> Arena<M> {
598 /// Create a new arena with default capacity (16 slots).
599 ///
600 /// For workloads protecting many distinct SEXPs (e.g., ppsize-scale loops),
601 /// prefer [`with_capacity`](Self::with_capacity) to avoid backing VECSXP
602 /// growth and map rehashing during operation.
603 ///
604 /// # Safety
605 ///
606 /// Must be called from the R main thread.
607 pub unsafe fn new() -> Self {
608 unsafe { Self::with_capacity(ArenaState::<M>::INITIAL_CAPACITY) }
609 }
610
611 /// Create a new arena with specific initial capacity.
612 ///
613 /// Pre-sizing the arena avoids growth of the backing VECSXP and rehashing
614 /// of the internal map. Use this when the expected number of distinct
615 /// protected values is known or can be estimated.
616 ///
617 /// # Safety
618 ///
619 /// Must be called from the R main thread.
620 pub unsafe fn with_capacity(capacity: usize) -> Self {
621 Self {
622 state: RefCell::new(unsafe { ArenaState::new(capacity) }),
623 _nosend: PhantomData,
624 }
625 }
626
627 /// Protect a SEXP, incrementing its reference count.
628 ///
629 /// # Safety
630 ///
631 /// Must be called from the R main thread.
632 #[inline]
633 pub unsafe fn protect(&self, x: SEXP) -> SEXP {
634 unsafe { self.state.borrow_mut().protect(x) }
635 }
636
637 /// Unprotect a SEXP, decrementing its reference count.
638 ///
639 /// # Safety
640 ///
641 /// Must be called from the R main thread.
642 ///
643 /// # Panics
644 ///
645 /// Panics if `x` was not protected by this arena.
646 #[inline]
647 pub unsafe fn unprotect(&self, x: SEXP) {
648 unsafe { self.state.borrow_mut().unprotect(x) };
649 }
650
651 /// Try to unprotect a SEXP, returning `true` if it was protected.
652 ///
653 /// # Safety
654 ///
655 /// Must be called from the R main thread.
656 #[inline]
657 pub unsafe fn try_unprotect(&self, x: SEXP) -> bool {
658 unsafe { self.state.borrow_mut().try_unprotect(x) }
659 }
660
661 /// Check if a SEXP is currently protected by this arena.
662 #[inline]
663 pub fn is_protected(&self, x: SEXP) -> bool {
664 self.state.borrow().is_protected(x)
665 }
666
667 /// Get the reference count for a SEXP (0 if not protected).
668 #[inline]
669 pub fn ref_count(&self, x: SEXP) -> usize {
670 self.state.borrow().ref_count(x)
671 }
672
673 /// Get the number of distinct SEXPs currently protected.
674 #[inline]
675 pub fn len(&self) -> usize {
676 self.state.borrow().len
677 }
678
679 /// Check if the arena is empty.
680 #[inline]
681 pub fn is_empty(&self) -> bool {
682 self.state.borrow().len == 0
683 }
684
685 /// Get the current capacity.
686 #[inline]
687 pub fn capacity(&self) -> usize {
688 self.state.borrow().capacity
689 }
690
691 /// Clear all protections.
692 ///
693 /// # Safety
694 ///
695 /// Must be called from the R main thread.
696 pub unsafe fn clear(&self) {
697 unsafe { self.state.borrow_mut().clear() };
698 }
699
700 /// Protect a SEXP and return an RAII guard.
701 ///
702 /// # Safety
703 ///
704 /// Must be called from the R main thread.
705 #[inline]
706 pub unsafe fn guard(&self, x: SEXP) -> ArenaGuard<'_, M> {
707 unsafe { ArenaGuard::new(self, x) }
708 }
709}
710
711impl<M: MapStorage> Drop for Arena<M> {
712 fn drop(&mut self) {
713 let state = self.state.get_mut();
714 // Drop the map first (always initialized for Arena<M>, which uses ArenaState::new()).
715 // SAFETY: Arena<M> always constructs via ArenaState::new() which initializes the map.
716 unsafe { state.map.assume_init_drop() };
717 unsafe { state.release_backing() };
718 }
719}
720
721impl<M: MapStorage> Default for Arena<M> {
722 fn default() -> Self {
723 unsafe { Self::new() }
724 }
725}
726// endregion
727
728// region: Type aliases for common arena types
729
730/// BTreeMap-based arena (default, good for reference counting).
731pub type RefCountedArena = Arena<BTreeMap<usize, Entry>>;
732
733/// HashMap-based arena (faster for large collections).
734pub type HashMapArena = Arena<HashMap<usize, Entry>>;
735// endregion
736
737// region: Fast hash types (feature-gated)
738
739/// HashMap with ahash for faster hashing (not DOS-resistant).
740///
741/// Uses ahash instead of SipHash for improved throughput. Not DOS-resistant,
742/// suitable for local, non-hostile environments.
743#[cfg(feature = "refcount-fast-hash")]
744pub type FastHashMap = std::collections::HashMap<usize, Entry, ahash::RandomState>;
745
746#[cfg(feature = "refcount-fast-hash")]
747impl_hashmap_map_storage!(FastHashMap);
748
749/// Fast hash arena using ahash (requires `refcount-fast-hash` feature).
750///
751/// Uses ahash instead of SipHash for improved throughput on large collections.
752/// Not DOS-resistant, suitable for local, non-hostile environments.
753#[cfg(feature = "refcount-fast-hash")]
754pub type FastHashMapArena = Arena<FastHashMap>;
755// endregion
756
757// region: RAII Guard
758
759/// An RAII guard that unprotects a SEXP when dropped.
760pub struct ArenaGuard<'a, M: MapStorage> {
761 arena: &'a Arena<M>,
762 sexp: SEXP,
763}
764
765impl<'a, M: MapStorage> ArenaGuard<'a, M> {
766 /// Create a new guard that protects the SEXP and unprotects on drop.
767 ///
768 /// # Safety
769 ///
770 /// Must be called from the R main thread. The SEXP must be valid.
771 #[inline]
772 pub unsafe fn new(arena: &'a Arena<M>, sexp: SEXP) -> Self {
773 unsafe { arena.protect(sexp) };
774 Self { arena, sexp }
775 }
776
777 #[inline]
778 /// Returns the protected SEXP.
779 pub fn get(&self) -> SEXP {
780 self.sexp
781 }
782}
783
784impl<M: MapStorage> Drop for ArenaGuard<'_, M> {
785 fn drop(&mut self) {
786 unsafe { self.arena.unprotect(self.sexp) };
787 }
788}
789
790impl<M: MapStorage> std::ops::Deref for ArenaGuard<'_, M> {
791 type Target = SEXP;
792
793 #[inline]
794 fn deref(&self) -> &Self::Target {
795 &self.sexp
796 }
797}
798
799/// Legacy type alias for backwards compatibility.
800pub type RefCountedGuard<'a> = ArenaGuard<'a, BTreeMap<usize, Entry>>;
801// endregion
802
803// region: Thread-local arena trait + macro
804
805/// Trait providing default implementations for all thread-local arena methods.
806///
807/// Implementors only need to provide [`with_state`](Self::with_state) to access
808/// the thread-local state; all 14 arena methods are provided as defaults.
809///
810/// The `define_thread_local_arena!` macro generates both the struct and the
811/// `ThreadLocalArenaOps` impl, so this trait is an implementation detail.
812/// Import it when calling methods on thread-local arena types:
813///
814/// ```ignore
815/// use miniextendr_api::refcount_protect::{ThreadLocalArena, ThreadLocalArenaOps};
816/// unsafe { ThreadLocalArena::protect(x) };
817/// ```
818pub trait ThreadLocalArenaOps {
819 /// The map storage type used by this arena.
820 type Map: MapStorage;
821
822 /// Access the thread-local state.
823 ///
824 /// Implementors route through `thread_local!` + `UnsafeCell`.
825 fn with_state<R, F: FnOnce(&mut ThreadLocalState<Self::Map>) -> R>(f: F) -> R;
826
827 /// Initialize the arena with default capacity (called automatically on first use).
828 ///
829 /// # Safety
830 ///
831 /// Must be called from the R main thread.
832 unsafe fn init() {
833 Self::with_state(|s| {
834 if !s.initialized {
835 unsafe { s.init() };
836 }
837 });
838 }
839
840 /// Initialize the arena with specific capacity.
841 ///
842 /// Use this when you know the expected number of distinct protected values
843 /// to avoid backing VECSXP growth and map rehashing during operation.
844 ///
845 /// If already initialized, this is a no-op.
846 ///
847 /// # Safety
848 ///
849 /// Must be called from the R main thread.
850 unsafe fn init_with_capacity(capacity: usize) {
851 Self::with_state(|s| {
852 if !s.initialized {
853 unsafe { s.init_with_capacity(capacity) };
854 }
855 });
856 }
857
858 /// Protect a SEXP, incrementing its reference count.
859 ///
860 /// # Safety
861 ///
862 /// Must be called from the R main thread.
863 #[inline]
864 unsafe fn protect(x: SEXP) -> SEXP {
865 Self::with_state(|s| {
866 if !s.initialized {
867 unsafe { s.init() };
868 }
869 unsafe { s.inner.protect(x) }
870 })
871 }
872
873 /// Unprotect a SEXP.
874 ///
875 /// # Safety
876 ///
877 /// Must be called from the R main thread.
878 #[inline]
879 unsafe fn unprotect(x: SEXP) {
880 Self::with_state(|s| {
881 // If the arena was never initialized, no SEXP could have been
882 // protected by it, so there is nothing to unprotect.
883 if !s.initialized {
884 return;
885 }
886 unsafe { s.inner.unprotect(x) };
887 });
888 }
889
890 /// Try to unprotect a SEXP.
891 ///
892 /// # Safety
893 ///
894 /// Must be called from the R main thread.
895 #[inline]
896 unsafe fn try_unprotect(x: SEXP) -> bool {
897 Self::with_state(|s| {
898 // If the arena was never initialized, no SEXP could have been
899 // protected by it, so return false.
900 if !s.initialized {
901 return false;
902 }
903 unsafe { s.inner.try_unprotect(x) }
904 })
905 }
906
907 /// Protect without checking initialization.
908 ///
909 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
910 ///
911 /// # Safety
912 ///
913 /// - Must be called from the R main thread.
914 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
915 #[inline]
916 unsafe fn protect_fast(x: SEXP) -> SEXP {
917 Self::with_state(|s| {
918 debug_assert!(s.initialized, "protect_fast called before init");
919 unsafe { s.inner.protect(x) }
920 })
921 }
922
923 /// Unprotect without checking initialization.
924 ///
925 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
926 ///
927 /// # Safety
928 ///
929 /// - Must be called from the R main thread.
930 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
931 #[inline]
932 unsafe fn unprotect_fast(x: SEXP) {
933 Self::with_state(|s| {
934 debug_assert!(s.initialized, "unprotect_fast called before init");
935 unsafe { s.inner.unprotect(x) };
936 });
937 }
938
939 /// Try to unprotect without checking initialization.
940 ///
941 /// For hot loops where `init()` or `init_with_capacity()` has already been called.
942 ///
943 /// # Safety
944 ///
945 /// - Must be called from the R main thread.
946 /// - The arena must have been initialized via `init()` or `init_with_capacity()`.
947 #[inline]
948 unsafe fn try_unprotect_fast(x: SEXP) -> bool {
949 Self::with_state(|s| {
950 debug_assert!(s.initialized, "try_unprotect_fast called before init");
951 unsafe { s.inner.try_unprotect(x) }
952 })
953 }
954
955 /// Check if a SEXP is protected.
956 #[inline]
957 fn is_protected(x: SEXP) -> bool {
958 Self::with_state(|s| {
959 if !s.initialized {
960 return false;
961 }
962 s.inner.is_protected(x)
963 })
964 }
965
966 /// Get reference count.
967 #[inline]
968 fn ref_count(x: SEXP) -> usize {
969 Self::with_state(|s| {
970 if !s.initialized {
971 return 0;
972 }
973 s.inner.ref_count(x)
974 })
975 }
976
977 /// Number of protected SEXPs.
978 #[inline]
979 fn len() -> usize {
980 Self::with_state(|s| s.inner.len)
981 }
982
983 /// Check if empty.
984 #[inline]
985 fn is_empty() -> bool {
986 Self::len() == 0
987 }
988
989 /// Get capacity.
990 #[inline]
991 fn capacity() -> usize {
992 Self::with_state(|s| s.inner.capacity)
993 }
994
995 /// Clear all protections.
996 ///
997 /// # Safety
998 ///
999 /// Must be called from the R main thread.
1000 unsafe fn clear() {
1001 Self::with_state(|s| {
1002 if s.initialized {
1003 unsafe { s.inner.clear() };
1004 }
1005 });
1006 }
1007}
1008
1009/// Macro to define a thread-local arena with a specific map type.
1010///
1011/// This creates a zero-sized struct implementing [`ThreadLocalArenaOps`],
1012/// providing all arena methods via the trait's default implementations.
1013///
1014/// # Example
1015///
1016/// ```ignore
1017/// define_thread_local_arena!(
1018/// /// My custom thread-local arena.
1019/// pub MyArena,
1020/// BTreeMap<usize, Entry>,
1021/// MY_ARENA_STATE
1022/// );
1023/// ```
1024#[macro_export]
1025macro_rules! define_thread_local_arena {
1026 (
1027 $(#[$meta:meta])*
1028 $vis:vis $name:ident,
1029 $map:ty,
1030 $state_name:ident
1031 ) => {
1032 thread_local! {
1033 static $state_name: std::cell::UnsafeCell<$crate::refcount_protect::ThreadLocalState<$map>> =
1034 const { std::cell::UnsafeCell::new($crate::refcount_protect::ThreadLocalState::uninit()) };
1035 }
1036
1037 $(#[$meta])*
1038 $vis struct $name;
1039
1040 impl $crate::refcount_protect::ThreadLocalArenaOps for $name {
1041 type Map = $map;
1042
1043 fn with_state<R, F: FnOnce(&mut $crate::refcount_protect::ThreadLocalState<$map>) -> R>(f: F) -> R {
1044 $state_name.with(|cell| f(unsafe { &mut *cell.get() }))
1045 }
1046 }
1047 };
1048}
1049
1050/// State wrapper for thread-local arenas (used by macro).
1051#[doc(hidden)]
1052pub struct ThreadLocalState<M: MapStorage> {
1053 pub inner: ArenaState<M>,
1054 pub initialized: bool,
1055}
1056
1057impl<M: MapStorage> ThreadLocalState<M> {
1058 /// Create an uninitialized thread-local arena state.
1059 ///
1060 /// Call `init` or `init_with_capacity` before use.
1061 pub const fn uninit() -> Self {
1062 Self {
1063 inner: ArenaState::uninit(),
1064 initialized: false,
1065 }
1066 }
1067
1068 /// Initialize with default capacity (16 slots).
1069 ///
1070 /// For ppsize-scale workloads, prefer [`init_with_capacity`](Self::init_with_capacity)
1071 /// to avoid backing VECSXP growth and map rehashing during operation.
1072 ///
1073 /// # Safety
1074 ///
1075 /// Must be called from the R main thread. Must only be called once.
1076 pub unsafe fn init(&mut self) {
1077 unsafe { self.inner.init(ArenaState::<M>::INITIAL_CAPACITY) };
1078 self.initialized = true;
1079 }
1080
1081 /// Initialize with specific capacity.
1082 ///
1083 /// Pre-sizing avoids growth of the backing VECSXP and rehashing of the
1084 /// internal map. Use this when the expected number of distinct protected
1085 /// values is known or can be estimated (e.g., the length of an input vector).
1086 ///
1087 /// # Safety
1088 ///
1089 /// Must be called from the R main thread. Must only be called once.
1090 pub unsafe fn init_with_capacity(&mut self, capacity: usize) {
1091 unsafe { self.inner.init(capacity) };
1092 self.initialized = true;
1093 }
1094}
1095
1096impl<M: MapStorage> Drop for ThreadLocalState<M> {
1097 fn drop(&mut self) {
1098 if self.initialized {
1099 // SAFETY: The map was initialized in init() or init_with_capacity().
1100 // We must manually drop it because MaybeUninit does not run Drop.
1101 unsafe { self.inner.map.assume_init_drop() };
1102 }
1103 // R backing is released separately via release_backing() if needed.
1104 // Thread-local destructors may run after R has shut down, so we do NOT
1105 // call R_ReleaseObject here — the R runtime owns the backing VECSXP
1106 // lifetime via R_PreserveObject.
1107 }
1108}
1109// endregion
1110
1111// region: Built-in thread-local arenas
1112
1113define_thread_local_arena!(
1114 /// Thread-local BTreeMap-based arena.
1115 ///
1116 /// This provides the lowest overhead for protection operations by
1117 /// eliminating RefCell borrow checking.
1118 pub ThreadLocalArena,
1119 BTreeMap<usize, Entry>,
1120 THREAD_LOCAL_BTREE_STATE
1121);
1122
1123define_thread_local_arena!(
1124 /// Thread-local HashMap-based arena.
1125 ///
1126 /// Combines HashMap's performance for large collections with
1127 /// thread-local storage's low overhead.
1128 pub ThreadLocalHashArena,
1129 HashMap<usize, Entry>,
1130 THREAD_LOCAL_HASH_STATE
1131);
1132// endregion
1133
1134// region: Fast hash thread-local arena (feature-gated)
1135
1136#[cfg(feature = "refcount-fast-hash")]
1137define_thread_local_arena!(
1138 /// Thread-local fast hash arena using ahash.
1139 ///
1140 /// Combines ahash's faster hashing with thread-local storage's low overhead.
1141 /// Ideal for hot loops protecting many distinct values.
1142 ///
1143 /// Not DOS-resistant, suitable for local, non-hostile environments.
1144 ///
1145 /// Requires the `refcount-fast-hash` feature.
1146 pub ThreadLocalFastHashArena,
1147 FastHashMap,
1148 THREAD_LOCAL_FAST_HASH_STATE
1149);
1150
1151// Tests are in tests/refcount_protect.rs (requires R runtime via miniextendr-engine)
1152// endregion