Skip to main content

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(#[automatically_derived]
impl ::core::marker::Copy for ProtectKey { }Copy, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ProtectKey { }
#[automatically_derived]
impl ::core::clone::Clone for ProtectKey {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<u32>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for ProtectKey {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "ProtectKey",
            "slot", &self.slot, "generation", &&self.generation)
    }
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ProtectKey { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ProtectKey {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.slot == other.slot && self.generation == other.generation
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ProtectKey {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u32>;
    }
}Eq, #[automatically_derived]
impl ::core::hash::Hash for ProtectKey {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.slot, state);
        ::core::hash::Hash::hash(&self.generation, state)
    }
}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: ::alloc::vec::from_elem(0, capacity)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}