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

ScenarioUse RAllocator?
Standalone binary embedding RYes
Arena-style allocation in .CallYes
#[global_allocator] in an R package lib crateNo β€” would be called at compile time when R isn’t available
Performance-critical hot loopsProbably 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

  1. Compute total size: alignment padding + Header (8 bytes) + requested size
  2. Rf_allocVector(RAWSXP, total) β€” allocate an R raw vector
  3. preserve::insert(sexp) β€” protect from GC (any-order release, not LIFO)
  4. Write the Header (preserve tag) immediately before the user pointer
  5. Return the aligned user pointer

πŸ”—Deallocation

  1. Read the Header just before the pointer β†’ recover preserve_tag
  2. preserve::release(tag) β€” R’s GC can now reclaim the RAWSXP

πŸ”—Reallocation

  1. Recover the original RAWSXP via the header’s preserve tag
  2. Check if the existing RAWSXP has spare capacity (possible due to alignment over-allocation)
  3. If it fits β†’ return the same pointer (no copy)
  4. 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 ThreadBehavior
R main threadExecutes 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_UnwindProtect catches the longjmp, Rust destructors run normally
  • Inside run_on_worker (with worker-thread feature): same protection via R_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 .Call invocations
  • 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.