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 R_PreserveObject and released on dealloc via
R_ReleaseObject.
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 sexp: SEXP pointing back to the RAWSXP that
backs this allocation. On dealloc, the allocator reads the header to recover
the SEXP and calls R_ReleaseObject to drop Rβs protection.
πHow It Works
πAllocation
- Compute total size: alignment padding +
Header(8 bytes) + requested size Rf_allocVector(RAWSXP, total)- allocate an R raw vectorR_PreserveObject(sexp)- protect from GC- Write the
Header(SEXP back-pointer) immediately before the user pointer - Return the aligned user pointer
πDeallocation
- Read the
Headerjust before the pointer β recover backingsexp R_ReleaseObject(sexp)- Rβs GC can now reclaim the RAWSXP
πReallocation
- Recover the original RAWSXP via the header
- 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, 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.