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    /// 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