Skip to main content

miniextendr_api/
protect_pool.rs

1//! VECSXP-backed protection pool with generational keys.
2//!
3//! A GC protection mechanism that stores protected SEXPs in a single R VECSXP
4//! (generic list), with slot management and generation tracking on the Rust side.
5//!
6//! # Performance
7//!
8//! Benchmarked at 10.1 ns/op for single insert+release. Zero R allocation per
9//! insert (unlike `R_PreserveObject`, which allocates a CONSXP each time).
10//! See `analysis/gc-protection-benchmarks-results.md` for full data.
11//!
12//! # When to use
13//!
14//! Use this for cross-`.Call` protection when:
15//! - You have many protected objects or frequent insert/release churn
16//! - You need any-order release (not LIFO)
17//! - You want generational safety (stale-key detection)
18//!
19//! For temporaries within a `.Call`, use [`ProtectScope`](crate::gc_protect::ProtectScope)
20//! instead (7.4 ns/op, zero allocation, LIFO bulk cleanup).
21//!
22//! `R_PreserveObject` is only appropriate for a *singleton* object that is
23//! released (if ever) in LIFO order relative to other preserved objects — e.g.
24//! this pool's own backing VECSXP. It is **not** appropriate for objects held
25//! in bulk and released in arbitrary or FIFO order: `R_ReleaseObject`'s O(n)
26//! precious-list scan degrades to O(n²) on exactly that pattern (60–65× slower
27//! than this pool at 10k objects; see
28//! `analysis/gc-protection-benchmarks-results.md`). `ExternalPtr` roots through
29//! this pool for that reason — a `Vec<ExternalPtr>` releases its roots
30//! front-to-back, the worst case for the precious list.
31//!
32//! # Architecture
33//!
34//! ```text
35//! ┌─────────────────────────────────────┐
36//! │  R side: VECSXP (GC-traced slots)   │  ← one R_PreserveObject, ever
37//! │  [SEXP][SEXP][NIL][SEXP][NIL][SEXP] │
38//! └──────┬──────────────────────────────┘
39//!        │ slot indices
40//! ┌──────┴──────────────────────────────┐
41//! │  Rust side: Vec<u32> generations    │  ← one free list, one generation array
42//! │  + Vec<usize> free_slots            │
43//! └─────────────────────────────────────┘
44//! ```
45//!
46//! No external dependencies for slot management. The generation counter per slot
47//! detects stale keys. Single free list for VECSXP slot reuse.
48
49use crate::sys::{R_PreserveObject, R_ReleaseObject, Rf_allocVector, Rf_protect, Rf_unprotect};
50use crate::{R_xlen_t, SEXP, SEXPTYPE, SexpExt};
51use std::marker::PhantomData;
52use std::rc::Rc;
53
54/// Generational key for a slot in a [`ProtectPool`].
55///
56/// Contains a slot index and a generation counter. If a slot is released and
57/// reused, the old key's generation won't match and operations will safely
58/// return `None` or no-op.
59///
60/// 8 bytes: 4-byte slot index + 4-byte generation.
61#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
62pub struct ProtectKey {
63    slot: u32,
64    generation: u32,
65}
66
67/// Enforces `!Send + !Sync` (R API is not thread-safe).
68type NoSendSync = PhantomData<Rc<()>>;
69
70/// A VECSXP-backed pool for GC protection with generational keys.
71///
72/// # Example
73///
74/// ```ignore
75/// let mut pool = unsafe { ProtectPool::new(16) };
76///
77/// let key = unsafe { pool.insert(some_sexp) };
78/// // SEXP is now protected from GC
79///
80/// let sexp = pool.get(key).unwrap();
81/// // Use the SEXP...
82///
83/// unsafe { pool.release(key) };
84/// // SEXP is no longer protected (eligible for GC)
85/// ```
86pub struct ProtectPool {
87    /// The VECSXP that holds protected SEXPs. Anchored by `R_PreserveObject`.
88    backing: SEXP,
89    /// Current capacity of the backing VECSXP.
90    capacity: usize,
91    /// Generation counter per VECSXP slot. Incremented on each release.
92    /// A key is valid iff `generations[key.slot] == key.generation`.
93    generations: Vec<u32>,
94    /// Free VECSXP slot indices for reuse.
95    free_slots: Vec<usize>,
96    /// Next fresh VECSXP slot index (for when free_slots is empty).
97    next_slot: usize,
98    /// Number of currently protected objects.
99    len: usize,
100    _nosend: NoSendSync,
101}
102
103impl ProtectPool {
104    /// Initial default capacity.
105    pub const DEFAULT_CAPACITY: usize = 16;
106
107    /// Create a new pool with the given initial capacity.
108    ///
109    /// # Safety
110    ///
111    /// Must be called from the R main thread.
112    pub unsafe fn new(capacity: usize) -> Self {
113        unsafe { Self::with_capacity(capacity) }
114    }
115
116    /// Create a new pool with a specific initial capacity.
117    ///
118    /// # Safety
119    ///
120    /// Must be called from the R main thread.
121    ///
122    /// # Panics
123    ///
124    /// Panics if `capacity` exceeds `R_xlen_t::MAX` or `u32::MAX`.
125    pub unsafe fn with_capacity(capacity: usize) -> Self {
126        let capacity = capacity.max(1);
127        let r_cap = R_xlen_t::try_from(capacity).expect("capacity exceeds R_xlen_t::MAX");
128        unsafe {
129            let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
130            R_PreserveObject(backing);
131            Rf_unprotect(1);
132
133            Self {
134                backing,
135                capacity,
136                generations: vec![0; capacity],
137                free_slots: Vec::with_capacity(capacity / 2),
138                next_slot: 0,
139                len: 0,
140                _nosend: PhantomData,
141            }
142        }
143    }
144
145    /// Protect a SEXP, returning a generational key.
146    ///
147    /// The SEXP will be protected from GC until [`release`](Self::release) is called
148    /// with the returned key. If the key is dropped without calling `release`, the
149    /// SEXP remains protected (leak, not crash).
150    ///
151    /// # Safety
152    ///
153    /// Must be called from the R main thread. `sexp` must be a valid SEXP.
154    ///
155    /// # Panics
156    ///
157    /// Panics if the pool has grown beyond `u32::MAX` slots.
158    #[inline]
159    pub unsafe fn insert(&mut self, sexp: SEXP) -> ProtectKey {
160        let slot = self.alloc_slot();
161        // slot < capacity ≤ R_xlen_t::MAX (checked in with_capacity/grow),
162        // so this conversion is safe.
163        let r_slot = R_xlen_t::try_from(slot).expect("slot exceeds R_xlen_t::MAX");
164        self.backing.set_vector_elt(r_slot, sexp);
165        self.len += 1;
166        ProtectKey {
167            slot: u32::try_from(slot).expect("slot exceeds u32::MAX"),
168            generation: self.generations[slot],
169        }
170    }
171
172    /// Release a previously protected SEXP.
173    ///
174    /// If the key is stale (already released, or from a different pool), this is a no-op.
175    ///
176    /// # Safety
177    ///
178    /// Must be called from the R main thread.
179    #[inline]
180    pub unsafe fn release(&mut self, key: ProtectKey) {
181        let Ok(slot) = usize::try_from(key.slot) else {
182            return;
183        };
184        let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
185            return;
186        };
187        if slot < self.generations.len() && self.generations[slot] == key.generation {
188            self.backing.set_vector_elt(r_slot, SEXP::nil());
189            self.generations[slot] = self.generations[slot].wrapping_add(1);
190            self.free_slots.push(slot);
191            self.len -= 1;
192        }
193    }
194
195    /// Get the SEXP for a key, or `None` if the key is stale.
196    #[inline]
197    pub fn get(&self, key: ProtectKey) -> Option<SEXP> {
198        let Ok(slot) = usize::try_from(key.slot) else {
199            return None;
200        };
201        let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
202            return None;
203        };
204        if slot < self.generations.len() && self.generations[slot] == key.generation {
205            Some(self.backing.vector_elt(r_slot))
206        } else {
207            None
208        }
209    }
210
211    /// Overwrite the SEXP at an existing key without releasing/reinserting.
212    ///
213    /// Returns `true` if the key was valid and the value was replaced.
214    /// Returns `false` if the key was stale (no-op).
215    ///
216    /// This is the pool equivalent of `R_Reprotect` — O(1), no allocation.
217    ///
218    /// # Safety
219    ///
220    /// Must be called from the R main thread. `sexp` must be a valid SEXP.
221    #[inline]
222    pub unsafe fn replace(&mut self, key: ProtectKey, sexp: SEXP) -> bool {
223        let Ok(slot) = usize::try_from(key.slot) else {
224            return false;
225        };
226        let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
227            return false;
228        };
229        if slot < self.generations.len() && self.generations[slot] == key.generation {
230            self.backing.set_vector_elt(r_slot, sexp);
231            true
232        } else {
233            false
234        }
235    }
236
237    /// Check if a key is currently valid (not stale).
238    #[inline]
239    pub fn contains_key(&self, key: ProtectKey) -> bool {
240        let Ok(slot) = usize::try_from(key.slot) else {
241            return false;
242        };
243        slot < self.generations.len() && self.generations[slot] == key.generation
244    }
245
246    /// Number of currently protected objects.
247    #[inline]
248    pub fn len(&self) -> usize {
249        self.len
250    }
251
252    /// Whether the pool is empty.
253    #[inline]
254    pub fn is_empty(&self) -> bool {
255        self.len == 0
256    }
257
258    /// Current capacity of the backing VECSXP.
259    #[inline]
260    pub fn capacity(&self) -> usize {
261        self.capacity
262    }
263
264    fn alloc_slot(&mut self) -> usize {
265        if let Some(slot) = self.free_slots.pop() {
266            return slot;
267        }
268        if self.next_slot >= self.capacity {
269            unsafe { self.grow() };
270        }
271        let slot = self.next_slot;
272        self.next_slot += 1;
273        slot
274    }
275
276    unsafe fn grow(&mut self) {
277        let new_cap = self
278            .capacity
279            .checked_mul(2)
280            .expect("ProtectPool capacity overflow");
281        let r_new_cap = R_xlen_t::try_from(new_cap).expect("new capacity exceeds R_xlen_t::MAX");
282        unsafe {
283            let new_backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_new_cap));
284            R_PreserveObject(new_backing);
285
286            for i in 0..self.capacity {
287                let r_i = R_xlen_t::try_from(i).expect("index exceeds R_xlen_t::MAX");
288                new_backing.set_vector_elt(r_i, self.backing.vector_elt(r_i));
289            }
290
291            R_ReleaseObject(self.backing);
292            Rf_unprotect(1);
293
294            self.backing = new_backing;
295            self.generations.resize(new_cap, 0);
296            self.capacity = new_cap;
297        }
298    }
299}
300
301impl Drop for ProtectPool {
302    fn drop(&mut self) {
303        unsafe { R_ReleaseObject(self.backing) };
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn pool_is_not_send() {
313        fn _assert_not_send<T: Send>() {}
314        // Uncomment to verify: _assert_not_send::<ProtectPool>();
315    }
316
317    #[test]
318    fn key_generational_safety() {
319        let mut gens: Vec<u32> = vec![0; 4];
320        let mut free: Vec<usize> = Vec::new();
321
322        let k1 = ProtectKey {
323            slot: 0,
324            generation: gens[0],
325        };
326        assert_eq!(gens[0], k1.generation);
327
328        gens[0] = gens[0].wrapping_add(1);
329        free.push(0);
330        assert_ne!(gens[0], k1.generation);
331
332        let slot = free.pop().unwrap();
333        let k2 = ProtectKey {
334            slot: u32::try_from(slot).unwrap(),
335            generation: gens[slot],
336        };
337        assert_eq!(gens[0], k2.generation);
338        assert_ne!(k1.generation, k2.generation);
339    }
340
341    #[test]
342    fn key_size() {
343        assert_eq!(std::mem::size_of::<ProtectKey>(), 8);
344    }
345}