Expand description
GC protection tools built on R’s PROTECT stack.
This module provides RAII wrappers around R’s GC protection primitives.
§Submodules
| Module | Contents |
|---|---|
tls | Thread-local convenience API — tls::protect(x) without passing &ProtectScope |
§Core Types
ProtectScope— RAII scope that callsUNPROTECT(n)on dropOwnedProtect— single-value RAII protect/unprotectRoot— lifetime-tied handle to a protected SEXPReprotectSlot—PROTECT_WITH_INDEX+REPROTECTfor mutable slots
§Protection Strategies in miniextendr
miniextendr provides three complementary protection mechanisms for different scenarios:
| Strategy | Module | Lifetime | Release Order | Use Case |
|---|---|---|---|---|
| PROTECT stack | gc_protect | Within .Call | LIFO (stack) | Temporary allocations |
| Preserve list | preserve | Across .Calls | Any order | Long-lived R objects |
| R ownership | ExternalPtr | Until R GCs | R decides | Rust data owned by R |
§When to Use Each
Use gc_protect (this module) when:
- You allocate R objects during a
.Calland need them protected until return - You want RAII-based automatic balancing of PROTECT/UNPROTECT
- Protection is short-lived (within a single function)
Use preserve when:
- Objects must survive across multiple
.Callinvocations - You need to release protections in arbitrary order
- Example:
RAllocatorbacking memory
Use ExternalPtr when:
- You want R to own a Rust value
- The Rust value should be dropped when R garbage collects the pointer
- You’re exposing Rust structs to R code
§Visual Overview
┌─────────────────────────────────────────────────────────────────┐
│ .Call("my_func", x) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ProtectScope::new() │ │
│ │ ├── protect(Rf_allocVector(...)) // temp allocation │ │
│ │ ├── protect(Rf_allocVector(...)) // another temp │ │
│ │ └── UNPROTECT(n) on scope drop │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ return SEXP │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ preserve (objects surviving across .Calls) │
│ ├── preserve::insert(sexp) // add to linked list │
│ ├── ... multiple .Calls ... // object stays protected │
│ └── preserve::release(cell) // remove when done │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ExternalPtr<MyStruct> (R owns Rust data) │
│ ├── Construction: temporary Rf_protect │
│ ├── Return to R → R owns the EXTPTRSXP │
│ └── R GC → finalizer runs → Rust Drop executes │
└─────────────────────────────────────────────────────────────────┘§Types in This Module
This module provides RAII wrappers around R’s GC protection primitives:
| Type | Purpose |
|---|---|
ProtectScope | Batch protection with automatic UNPROTECT(n) on drop |
Root<'scope> | Lightweight handle tied to a scope’s lifetime |
OwnedProtect | Single-value RAII guard for simple cases |
ReprotectSlot<'scope> | Protected slot supporting replace-under-protection |
§Design Principles
ProtectScopeowns the responsibility of callingUNPROTECT(n)Root<'a>is a move-friendly, non-dropping handle whose lifetime ties to the scopeReprotectSlot<'a>supports replace-under-protection viaPROTECT_WITH_INDEX/REPROTECT
§Safety Model
These tools are unsafe to create because they require:
- Running on the R main thread - R’s API is not thread-safe
- No panics across FFI - Rust panics must not unwind across C boundary
- Understanding R errors - If R raises an error (
longjmp), Rust destructors will not run, so scope-based unprotection will leak
For cleanup that survives R errors, use R_UnwindProtect boundaries in your
.Call trampoline (see unwind_protect).
§Example
use miniextendr_api::gc_protect::ProtectScope;
use miniextendr_api::ffi::SEXP;
unsafe fn process_vectors(x: SEXP, y: SEXP) -> SEXP {
let scope = ProtectScope::new();
// Protect multiple values
let x = scope.protect(x);
let y = scope.protect(y);
// Work with protected values...
let result = scope.protect(some_r_function(x.get(), y.get()));
result.into_raw()
} // UNPROTECT(3) called automatically§Container Insertion Patterns
When building containers (lists, character vectors), children need protection between allocation and insertion:
// WRONG - child unprotected between allocation and SET_VECTOR_ELT
let child = Rf_allocVector(REALSXP, 10); // unprotected!
list.set_vector_elt(0, child); // GC could occur before this!
// CORRECT - use safe insertion methods
let list = List::from_raw(scope.alloc_vecsxp(n).into_raw());
for i in 0..n {
let child = Rf_allocVector(REALSXP, 10);
list.set_elt(i, child); // protects child during insertion
}
// EFFICIENT - use ListBuilder with scope
let builder = ListBuilder::new(&scope, n);
for i in 0..n {
let child = scope.alloc_real(10).into_raw();
builder.set(i, child); // child already protected by scope
}See List::set_elt,
ListBuilder, and
StrVec::set_str for safe container APIs.
§Reassignment with ReprotectSlot
Use ReprotectSlot when you need to reassign a protected value multiple times
without growing the protection stack:
let slot = scope.protect_with_index(initial_value);
for item in items {
let new_value = process(slot.get(), item);
slot.set(new_value); // R_Reprotect, stack count unchanged
}This avoids the LIFO drop-order pitfall of reassigning OwnedProtect guards.
Modules§
- tls
- TLS-backed convenience API for GC protection.
Structs§
- Owned
Protect - A single-object RAII guard:
PROTECTon create,UNPROTECT(1)on drop. - Protect
Scope - A scope that automatically balances
UNPROTECT(n)on drop. - Reprotect
Slot - A protected slot created with
R_ProtectWithIndexand updated withR_Reprotect. - Root
- A rooted SEXP tied to the lifetime of a
ProtectScope. - Worker
Unprotect Guard - A
Send-safe guard that callsRf_unprotect(n)on drop viawith_r_thread.
Traits§
- Protector
- A scope-like GC protection backend.
Type Aliases§
- NoSend
Sync 🔒 - Enforces
!Send + !Sync(R API is not thread-safe). - Protect
Index - R’s PROTECT_INDEX type (just
c_intunder the hood).