miniextendr_api/protect_pool.rs
1//! [`ProtectPool`]: an owned root set for R objects, held in one R list.
2//!
3//! The pool stores the objects it roots in a single VECSXP (generic list) that
4//! R's collector traces, and keeps the slot bookkeeping and a generation per
5//! slot on the Rust side. One `R_PreserveObject` roots that list. Dropping the
6//! pool releases it, and with it every object the pool holds.
7//!
8//! # The per-call root set
9//!
10//! Code that makes intermediate R objects and needs them alive until its
11//! `.Call` returns (attribute vectors, results of R calls, sliced columns)
12//! roots each one with [`root`](ProtectPool::root), which returns the object:
13//!
14//! ```ignore
15//! struct Columns {
16//! roots: ProtectPool,
17//! cols: Vec<(String, SEXP)>,
18//! }
19//!
20//! impl Columns {
21//! fn push(&mut self, name: &str, value: impl IntoR) {
22//! let sexp = self.roots.root(value);
23//! self.cols.push((name.to_owned(), sexp));
24//! }
25//! }
26//! ```
27//!
28//! A struct can own the pool and drop in any order relative to other pools,
29//! scopes and guards. A [`ProtectScope`](crate::ProtectScope) moved into a
30//! struct cannot: it unprotects with `UNPROTECT(n)`, which pops the top `n`
31//! entries of R's protect stack whoever pushed them, so dropping it out of
32//! order unprotects another scope's values. The pool takes no protect-stack
33//! entry per value.
34//!
35//! # The cross-call pool
36//!
37//! Objects that outlive the `.Call` that made them and are released one at a
38//! time, in any order: [`insert`](ProtectPool::insert) returns a generational
39//! [`ProtectKey`], and [`release`](ProtectPool::release) frees its slot in
40//! O(1). Every owning [`ExternalPtr`](struct@crate::ExternalPtr) is rooted in
41//! one process-wide pool.
42//!
43//! # Contract
44//!
45//! The pool has the contract of [`Preserved`](crate::Preserved) and the other
46//! owned roots: its functions are safe to call on R's main thread, and the type
47//! is `!Send + !Sync`, so it is made, used and dropped there. An object stays
48//! rooted while it occupies a slot: until its key is released, its slot is
49//! replaced, or the pool drops. A [`SEXP`] copied out of the pool is not rooted
50//! on its own after that.
51//!
52//! # Performance
53//!
54//! Benchmarked at 10.1 ns/op for single insert+release. Zero R allocation per
55//! insert (unlike `R_PreserveObject`, which allocates a CONSXP each time). The
56//! backing list doubles when it is full.
57//! See `analysis/gc-protection-benchmarks-results.md` for full data.
58//!
59//! # When to use
60//!
61//! - A temporary within one call: [`ProtectScope`](crate::ProtectScope)
62//! (7.4 ns/op, LIFO bulk cleanup).
63//! - Many values, any drop order, or owned by a struct: this pool.
64//! - One long-lived object: [`Preserved`](crate::Preserved).
65//!
66//! `R_PreserveObject` is only appropriate for a *singleton* object that is
67//! released (if ever) in LIFO order relative to other preserved objects — e.g.
68//! this pool's own backing VECSXP. It is **not** appropriate for objects held
69//! in bulk and released in arbitrary or FIFO order: `R_ReleaseObject`'s O(n)
70//! precious-list scan degrades to O(n²) on exactly that pattern (60–65× slower
71//! than this pool at 10k objects; see
72//! `analysis/gc-protection-benchmarks-results.md`). `ExternalPtr` roots through
73//! this pool for that reason — a `Vec<ExternalPtr>` releases its roots
74//! front-to-back, the worst case for the precious list.
75//!
76//! # Architecture
77//!
78//! ```text
79//! ┌─────────────────────────────────────┐
80//! │ R side: VECSXP (GC-traced slots) │ ← one R_PreserveObject at a time
81//! │ [SEXP][SEXP][NIL][SEXP][NIL][SEXP] │
82//! └──────┬──────────────────────────────┘
83//! │ slot indices
84//! ┌──────┴──────────────────────────────┐
85//! │ Rust side: Vec<u32> generations │ ← one free list, one generation array
86//! │ + Vec<usize> free_slots │
87//! └─────────────────────────────────────┘
88//! ```
89//!
90//! No external dependencies for slot management. The generation counter per slot
91//! detects stale keys. Single free list for VECSXP slot reuse.
92
93use crate::gc_protect::OwnedProtect;
94use crate::into_r::IntoR;
95use crate::sys::{R_PreserveObject, R_ReleaseObject, Rf_allocVector, Rf_protect, Rf_unprotect};
96use crate::{R_xlen_t, SEXP, SEXPTYPE, SexpExt};
97use std::marker::PhantomData;
98use std::rc::Rc;
99
100/// Generational key for a slot in a [`ProtectPool`].
101///
102/// Contains a slot index and a generation counter. Once a slot is released,
103/// its old keys are stale: the pool's generation for the slot no longer
104/// matches, so operations with them return `None` or do nothing. A key does
105/// not record its pool. Use it only with the pool that returned it: another
106/// pool treats it as a key of its own slot.
107///
108/// 8 bytes: 4-byte slot index + 4-byte generation.
109#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
110pub struct ProtectKey {
111 slot: u32,
112 generation: u32,
113}
114
115/// Enforces `!Send + !Sync` (R API is not thread-safe).
116type NoSendSync = PhantomData<Rc<()>>;
117
118/// An owned root set for R objects, held in one R list, with generational keys.
119///
120/// See the [module docs](self) for the two uses: the per-call root set
121/// ([`root`](Self::root)) and the cross-call pool ([`insert`](Self::insert) /
122/// [`release`](Self::release)).
123///
124/// # Example
125///
126/// ```ignore
127/// let mut pool = ProtectPool::new(ProtectPool::DEFAULT_CAPACITY);
128///
129/// // Root a value until the pool drops.
130/// let ints = pool.root(vec![1, 2, 3]);
131///
132/// // Root a value with a key, to release it early.
133/// let key = pool.insert(SEXP::scalar_integer(42));
134/// let sexp = pool.get(key).unwrap();
135/// pool.release(key);
136/// assert!(pool.get(key).is_none());
137/// ```
138///
139/// The pool is `!Send` and `!Sync`: R's API belongs to its main thread.
140///
141/// ```compile_fail
142/// fn assert_send<T: Send>() {}
143/// assert_send::<miniextendr_api::ProtectPool>();
144/// ```
145///
146/// ```compile_fail
147/// fn assert_sync<T: Sync>() {}
148/// assert_sync::<miniextendr_api::ProtectPool>();
149/// ```
150pub struct ProtectPool {
151 /// The VECSXP that holds protected SEXPs. Anchored by `R_PreserveObject`.
152 backing: SEXP,
153 /// Current capacity of the backing VECSXP.
154 capacity: usize,
155 /// Generation counter per VECSXP slot. Incremented on each release.
156 /// A key is valid iff `generations[key.slot] == key.generation`.
157 generations: Vec<u32>,
158 /// Free VECSXP slot indices for reuse.
159 free_slots: Vec<usize>,
160 /// Next fresh VECSXP slot index (for when free_slots is empty).
161 next_slot: usize,
162 /// Number of currently protected objects.
163 len: usize,
164 _nosend: NoSendSync,
165}
166
167impl ProtectPool {
168 /// Initial default capacity.
169 pub const DEFAULT_CAPACITY: usize = 16;
170
171 /// Create a pool with room for `capacity` objects before it grows.
172 ///
173 /// Same as [`with_capacity`](Self::with_capacity). Call it on R's main
174 /// thread.
175 ///
176 /// # Panics
177 ///
178 /// Panics if `capacity` exceeds `R_xlen_t::MAX`.
179 pub fn new(capacity: usize) -> Self {
180 Self::with_capacity(capacity)
181 }
182
183 /// Create a pool with room for `capacity` objects before it grows.
184 ///
185 /// Allocates the backing list and preserves it. Call it on R's main
186 /// thread.
187 ///
188 /// # Panics
189 ///
190 /// Panics if `capacity` exceeds `R_xlen_t::MAX`.
191 pub fn with_capacity(capacity: usize) -> Self {
192 let capacity = capacity.max(1);
193 let r_cap = R_xlen_t::try_from(capacity).expect("capacity exceeds R_xlen_t::MAX");
194 // SAFETY: R's main thread (the pool's contract). The new list stays
195 // protected while `R_PreserveObject` allocates its cell.
196 let backing = unsafe {
197 let backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_cap));
198 R_PreserveObject(backing);
199 Rf_unprotect(1);
200 backing
201 };
202
203 Self {
204 backing,
205 capacity,
206 generations: vec![0; capacity],
207 free_slots: Vec::with_capacity(capacity / 2),
208 next_slot: 0,
209 len: 0,
210 _nosend: PhantomData,
211 }
212 }
213
214 /// Convert `value` to an R object, root it until the pool drops, and
215 /// return it.
216 ///
217 /// The "keep this alive and give it back" call of the per-call root set.
218 /// A [`SEXP`] converts to itself, so `pool.root(sexp)` roots an existing
219 /// object, fresh and unprotected or not. Nothing allocates between the
220 /// conversion and the insert except the pool's own growth, which keeps the
221 /// object reachable (see [`insert`](Self::insert)).
222 ///
223 /// There is no key: the object is released with the pool. Use
224 /// [`insert`](Self::insert) to release it earlier. Call it on R's main
225 /// thread.
226 #[inline]
227 pub fn root(&mut self, value: impl IntoR) -> SEXP {
228 let sexp = value.into_sexp();
229 self.insert(sexp);
230 sexp
231 }
232
233 /// Root `sexp`, returning a generational key.
234 ///
235 /// `sexp` stays rooted until [`release`](Self::release) is called with
236 /// the key, its slot is [`replace`](Self::replace)d, or the pool drops. A
237 /// key dropped without `release` keeps it rooted until the pool drops.
238 ///
239 /// `sexp` may be a fresh, unprotected object. Inserting into a free slot
240 /// allocates nothing. When the pool is full it grows, which allocates a
241 /// new backing list; `sexp` stays on R's protect stack until then, so the
242 /// collection that allocation can trigger cannot reclaim it. Call it on R's
243 /// main thread.
244 ///
245 /// # Panics
246 ///
247 /// Panics if the pool has grown beyond `u32::MAX` slots.
248 #[inline]
249 pub fn insert(&mut self, sexp: SEXP) -> ProtectKey {
250 // Only growth allocates. The guard drops at the end of this call, after
251 // `grow` has popped its own entry, so the stack stays LIFO.
252 let _pending = self.is_full().then(|| {
253 // SAFETY: R's main thread (the pool's contract).
254 unsafe { OwnedProtect::new(sexp) }
255 });
256 let slot = self.alloc_slot();
257 // slot < capacity ≤ R_xlen_t::MAX (checked in with_capacity/grow),
258 // so this conversion is safe.
259 let r_slot = R_xlen_t::try_from(slot).expect("slot exceeds R_xlen_t::MAX");
260 self.backing.set_vector_elt(r_slot, sexp);
261 self.len += 1;
262 ProtectKey {
263 slot: u32::try_from(slot).expect("slot exceeds u32::MAX"),
264 generation: self.generations[slot],
265 }
266 }
267
268 /// Release the object `key` roots and free its slot for reuse.
269 ///
270 /// A stale key (already released) is a no-op. Allocates nothing. Call it
271 /// on R's main thread.
272 #[inline]
273 pub fn release(&mut self, key: ProtectKey) {
274 let Ok(slot) = usize::try_from(key.slot) else {
275 return;
276 };
277 let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
278 return;
279 };
280 if slot < self.generations.len() && self.generations[slot] == key.generation {
281 self.backing.set_vector_elt(r_slot, SEXP::nil());
282 self.generations[slot] = self.generations[slot].wrapping_add(1);
283 self.free_slots.push(slot);
284 self.len -= 1;
285 }
286 }
287
288 /// Get the SEXP for a key, or `None` if the key is stale.
289 #[inline]
290 pub fn get(&self, key: ProtectKey) -> Option<SEXP> {
291 let Ok(slot) = usize::try_from(key.slot) else {
292 return None;
293 };
294 let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
295 return None;
296 };
297 if slot < self.generations.len() && self.generations[slot] == key.generation {
298 Some(self.backing.vector_elt(r_slot))
299 } else {
300 None
301 }
302 }
303
304 /// Root `sexp` in `key`'s slot, in place of the object there.
305 ///
306 /// Returns `true` if the key was valid and the value was replaced. The
307 /// previous object is no longer rooted by this slot. Returns `false` for a
308 /// stale key and does nothing: `sexp` is then not rooted.
309 ///
310 /// This is the pool equivalent of `R_Reprotect`: O(1), and it allocates
311 /// nothing, so a fresh `sexp` is safe. Call it on R's main thread.
312 #[inline]
313 pub fn replace(&mut self, key: ProtectKey, sexp: SEXP) -> bool {
314 let Ok(slot) = usize::try_from(key.slot) else {
315 return false;
316 };
317 let Ok(r_slot) = R_xlen_t::try_from(key.slot) else {
318 return false;
319 };
320 if slot < self.generations.len() && self.generations[slot] == key.generation {
321 self.backing.set_vector_elt(r_slot, sexp);
322 true
323 } else {
324 false
325 }
326 }
327
328 /// Check if a key is currently valid (not stale).
329 #[inline]
330 pub fn contains_key(&self, key: ProtectKey) -> bool {
331 let Ok(slot) = usize::try_from(key.slot) else {
332 return false;
333 };
334 slot < self.generations.len() && self.generations[slot] == key.generation
335 }
336
337 /// Number of currently protected objects.
338 #[inline]
339 pub fn len(&self) -> usize {
340 self.len
341 }
342
343 /// Whether the pool is empty.
344 #[inline]
345 pub fn is_empty(&self) -> bool {
346 self.len == 0
347 }
348
349 /// Current capacity of the backing VECSXP.
350 #[inline]
351 pub fn capacity(&self) -> usize {
352 self.capacity
353 }
354
355 /// Whether the next insert grows the backing list.
356 #[inline]
357 fn is_full(&self) -> bool {
358 self.free_slots.is_empty() && self.next_slot >= self.capacity
359 }
360
361 fn alloc_slot(&mut self) -> usize {
362 if let Some(slot) = self.free_slots.pop() {
363 return slot;
364 }
365 if self.next_slot >= self.capacity {
366 self.grow();
367 }
368 let slot = self.next_slot;
369 self.next_slot += 1;
370 slot
371 }
372
373 /// Double the backing list. It allocates, so the caller keeps any object
374 /// it is about to store reachable.
375 fn grow(&mut self) {
376 let new_cap = self
377 .capacity
378 .checked_mul(2)
379 .expect("ProtectPool capacity overflow");
380 let r_new_cap = R_xlen_t::try_from(new_cap).expect("new capacity exceeds R_xlen_t::MAX");
381 // SAFETY: R's main thread (the pool's contract). The new list stays
382 // protected while `R_PreserveObject` allocates; copying and releasing
383 // allocate nothing.
384 unsafe {
385 let new_backing = Rf_protect(Rf_allocVector(SEXPTYPE::VECSXP, r_new_cap));
386 R_PreserveObject(new_backing);
387
388 for i in 0..self.capacity {
389 let r_i = R_xlen_t::try_from(i).expect("index exceeds R_xlen_t::MAX");
390 new_backing.set_vector_elt(r_i, self.backing.vector_elt(r_i));
391 }
392
393 R_ReleaseObject(self.backing);
394 Rf_unprotect(1);
395
396 self.backing = new_backing;
397 }
398 self.generations.resize(new_cap, 0);
399 self.capacity = new_cap;
400 }
401}
402
403impl Drop for ProtectPool {
404 fn drop(&mut self) {
405 // SAFETY: `!Send` keeps the drop on R's main thread. Releasing the
406 // backing list releases every object the pool holds at once, whatever
407 // order pools, scopes and guards drop in, and allocates nothing.
408 unsafe { R_ReleaseObject(self.backing) };
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn key_generational_safety() {
418 let mut gens: Vec<u32> = vec![0; 4];
419 let mut free: Vec<usize> = Vec::new();
420
421 let k1 = ProtectKey {
422 slot: 0,
423 generation: gens[0],
424 };
425 assert_eq!(gens[0], k1.generation);
426
427 gens[0] = gens[0].wrapping_add(1);
428 free.push(0);
429 assert_ne!(gens[0], k1.generation);
430
431 let slot = free.pop().unwrap();
432 let k2 = ProtectKey {
433 slot: u32::try_from(slot).unwrap(),
434 generation: gens[slot],
435 };
436 assert_eq!(gens[0], k2.generation);
437 assert_ne!(k1.generation, k2.generation);
438 }
439
440 #[test]
441 fn key_size() {
442 assert_eq!(std::mem::size_of::<ProtectKey>(), 8);
443 }
444}