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