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}