Skip to main content

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