Reference page
R-Backed Global Allocator
This document covers RAllocator, a Rust GlobalAlloc implementation backed by R's memory manager.
This document covers RAllocator, a Rust GlobalAlloc implementation backed
by Rβs memory manager.
πOverview
RAllocator routes every Rust heap allocation through Rβs Rf_allocVector(RAWSXP, n),
so Rust memory participates in Rβs garbage collection. Each allocation is
GC-protected via the preserve list and released
on dealloc.
Source: miniextendr-api/src/allocator.rs
πWhen to Use
| Scenario | Use RAllocator? |
|---|---|
| Standalone binary embedding R | Yes |
Arena-style allocation in .Call | Yes |
#[global_allocator] in an R package lib crate | No β would be called at compile time when R isnβt available |
| Performance-critical hot loops | Probably not β system allocator is faster |
πMemory Layout
Each allocation creates one R RAWSXP vector. Inside its data region:
RAWSXP data bytes:
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ
β alignment pad β Header β user bytes ... β
β (0..align-1) β (8 bytes) β β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
β²
βββ pointer returned to caller
The Header stores a single preserve_tag: SEXP β the cell in the preserve
list that keeps this RAWSXP alive. On dealloc, the allocator reads the header
to recover the tag and releases the preserve cell.
πHow It Works
πAllocation
- Compute total size: alignment padding +
Header(8 bytes) + requested size Rf_allocVector(RAWSXP, total)β allocate an R raw vectorpreserve::insert(sexp)β protect from GC (any-order release, not LIFO)- Write the
Header(preserve tag) immediately before the user pointer - Return the aligned user pointer
πDeallocation
- Read the
Headerjust before the pointer β recoverpreserve_tag preserve::release(tag)β Rβs GC can now reclaim the RAWSXP
πReallocation
- Recover the original RAWSXP via the headerβs preserve tag
- Check if the existing RAWSXP has spare capacity (possible due to alignment over-allocation)
- If it fits β return the same pointer (no copy)
- Otherwise β allocate new RAWSXP, copy data, release old
πZero-Sized Types
ZST allocations (layout.size() == 0) return null. Thereβs no RAWSXP to
track, and the dangling pointer convention would crash in dealloc when trying
to read the header.
πThread Safety
All R API calls are routed to the main thread automatically:
| Calling Thread | Behavior |
|---|---|
| R main thread | Executes directly (default path) |
Worker thread (with worker-thread feature, inside run_on_worker) | Routes via with_r_thread |
| Other threads (Rayon, spawned) | Panics |
The panic on arbitrary threads is intentional β Rβs C API is not thread-safe, and silently corrupting Rβs heap would be worse than a loud failure.
πCaveats
πlongjmp Risk
Rf_allocVector can longjmp on allocation failure instead of returning NULL.
If this happens:
- Inside
with_r_unwind_protect(default path):R_UnwindProtectcatches the longjmp, Rust destructors run normally - Inside
run_on_worker(withworker-threadfeature): same protection viaR_UnwindProtect - Outside protected context: Rust destructors are skipped, causing resource leaks (files, locks, etc.)
Best practice: use RAllocator inside with_r_unwind_protect or the worker
thread pattern β contexts where unwind protection is active.
πProtection Strategy
The allocator uses the preserve list (not the PROTECT stack) because:
- Allocations may survive across multiple
.Callinvocations - Deallocations happen in arbitrary order (not LIFO)
- The preserve list supports O(1) insert and any-order release
See GC Protection for the full picture of protection strategies.
πExample
use miniextendr_api::RAllocator;
// In a standalone binary that embeds R:
#[global_allocator]
static ALLOC: RAllocator = RAllocator;
fn main() {
// All Vec, String, Box, etc. allocations now go through R
let v = vec![1, 2, 3]; // backed by RAWSXP
}
Do NOT do this in an R package library crate β the allocator would be
invoked during cargo build before R is available.