Skip to main content

Module protect_pool

Module protect_pool 

Source
Expand description

ProtectPool: an owned root set for R objects, held in one R list.

The pool stores the objects it roots in a single VECSXP (generic list) that R’s collector traces, and keeps the slot bookkeeping and a generation per slot on the Rust side. One R_PreserveObject roots that list. Dropping the pool releases it, and with it every object the pool holds.

§The per-call root set

Code that makes intermediate R objects and needs them alive until its .Call returns (attribute vectors, results of R calls, sliced columns) roots each one with root, which returns the object:

ⓘ
struct Columns {
    roots: ProtectPool,
    cols: Vec<(String, SEXP)>,
}

impl Columns {
    fn push(&mut self, name: &str, value: impl IntoR) {
        let sexp = self.roots.root(value);
        self.cols.push((name.to_owned(), sexp));
    }
}

A struct can own the pool and drop in any order relative to other pools, scopes and guards. A ProtectScope moved into a struct cannot: it unprotects with UNPROTECT(n), which pops the top n entries of R’s protect stack whoever pushed them, so dropping it out of order unprotects another scope’s values. The pool takes no protect-stack entry per value.

§The cross-call pool

Objects that outlive the .Call that made them and are released one at a time, in any order: insert returns a generational ProtectKey, and release frees its slot in O(1). Every owning ExternalPtr is rooted in one process-wide pool.

§Contract

The pool has the contract of Preserved and the other owned roots: its functions are safe to call on R’s main thread, and the type is !Send + !Sync, so it is made, used and dropped there. An object stays rooted while it occupies a slot: until its key is released, its slot is replaced, or the pool drops. A SEXP copied out of the pool is not rooted on its own after that.

§Performance

Benchmarked at 10.1 ns/op for single insert+release. Zero R allocation per insert (unlike R_PreserveObject, which allocates a CONSXP each time). The backing list doubles when it is full. See analysis/gc-protection-benchmarks-results.md for full data.

§When to use

  • A temporary within one call: ProtectScope (7.4 ns/op, LIFO bulk cleanup).
  • Many values, any drop order, or owned by a struct: this pool.
  • One long-lived object: Preserved.

R_PreserveObject is only appropriate for a singleton object that is released (if ever) in LIFO order relative to other preserved objects — e.g. this pool’s own backing VECSXP. It is not appropriate for objects held in bulk and released in arbitrary or FIFO order: R_ReleaseObject’s O(n) precious-list scan degrades to O(n²) on exactly that pattern (60–65× slower than this pool at 10k objects; see analysis/gc-protection-benchmarks-results.md). ExternalPtr roots through this pool for that reason — a Vec<ExternalPtr> releases its roots front-to-back, the worst case for the precious list.

§Architecture

┌─────────────────────────────────────┐
│  R side: VECSXP (GC-traced slots)   │  ← one R_PreserveObject at a time
│  [SEXP][SEXP][NIL][SEXP][NIL][SEXP] │
└──────┬──────────────────────────────┘
       │ slot indices
┌──────┴──────────────────────────────┐
│  Rust side: Vec<u32> generations    │  ← one free list, one generation array
│  + Vec<usize> free_slots            │
└─────────────────────────────────────┘

No external dependencies for slot management. The generation counter per slot detects stale keys. Single free list for VECSXP slot reuse.

Structs§

ProtectKey
Generational key for a slot in a ProtectPool.
ProtectPool
An owned root set for R objects, held in one R list, with generational keys.

Type Aliases§

NoSendSync 🔒
Enforces !Send + !Sync (R API is not thread-safe).