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

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 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

  1. Compute total size: alignment padding + Header (8 bytes) + requested size
  2. Rf_allocVector(RAWSXP, total) - allocate an R raw vector
  3. R_PreserveObject(sexp) - protect from GC
  4. Write the Header (SEXP back-pointer) immediately before the user pointer
  5. Return the aligned user pointer

πŸ”—Deallocation

  1. Read the Header just before the pointer β†’ recover backing sexp
  2. R_ReleaseObject(sexp) - R’s GC can now reclaim the RAWSXP

πŸ”—Reallocation

  1. Recover the original RAWSXP via the header
  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, 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.