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}