This document explains how to safely call R APIs from threads other than the main R thread.

๐Ÿ”—The Problem

Rโ€™s API is designed to be called from a single thread - the main R thread. When you spawn a new thread and try to call R functions, youโ€™ll get a segfault. This happens because of Rโ€™s stack checking mechanism.

๐Ÿ”—How Rโ€™s Stack Checking Works

R tracks three global variables (defined in Rinterface.h, all non-API):

uintptr_t R_CStackStart;  // Top of the main thread's stack
uintptr_t R_CStackLimit;  // Stack size limit
int R_CStackDir;          // Stack growth direction (-1 = down, 1 = up)

During initialization (Rf_initialize_R), R sets these based on the main threadโ€™s stack:

  • Unix: Uses getrlimit(RLIMIT_STACK), __libc_stack_end, or KERN_USRSTACK
  • Windows: Uses VirtualQuery to determine stack bounds

Many R API functions call R_CheckStack():

void R_CheckStack(void) {
    int dummy;
    intptr_t usage = R_CStackDir * (R_CStackStart - (uintptr_t)&dummy);

    if (R_CStackLimit != -1 && usage > ((intptr_t) R_CStackLimit))
        R_SignalCStackOverflow(usage);
}

When called from a different thread, &dummy points to a completely different stack, causing:

  • usage to be a huge negative or positive number
  • False stack overflow detection
  • Segfault or abort

๐Ÿ”—The Solution

Setting R_CStackLimit to (uintptr_t)-1 (i.e., usize::MAX) disables stack checking entirely.

From R source (src/include/Defn.h):

if(R_CStackLimit != (uintptr_t)(-1) && usage > ((intptr_t) R_CStackLimit))

This is safe because:

  1. The OS still enforces real stack limits
  2. R functions correctly, just without its own overflow detection

๐Ÿ”—Using miniextendrโ€™s Thread Utilities

All thread utilities require the nonapi feature since they access non-API R internals.

[dependencies]
miniextendr-api = { version = "...", features = ["nonapi"] }

๐Ÿ”—Checked vs Unchecked R FFI

Most miniextendr_api::ffi::* functions are checked (via #[r_ffi_checked]). By default, they verify youโ€™re on the main thread and panic otherwise. With the worker-thread feature, if called from the worker thread, they route to the main thread via with_r_thread.

When you intentionally call R from a non-main thread using this module, use the *_unchecked variants if you want to bypass routing and you are certain youโ€™re on the main thread already.

๐Ÿ”—Simple Spawning: spawn_with_r

use miniextendr_api::spawn_with_r;

let handle = spawn_with_r(|| {
    // Safe to call R APIs here!
    unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) }
})?;

let result = handle.join().unwrap();

This function:

  1. Sets stack size to 8 MiB (configurable)
  2. Automatically disables Rโ€™s stack checking
  3. Restores stack checking when the thread completes

๐Ÿ”—Custom Configuration: RThreadBuilder

use miniextendr_api::{RThreadBuilder, WINDOWS_R_STACK_SIZE};

let handle = RThreadBuilder::new()
    .stack_size(WINDOWS_R_STACK_SIZE)  // 64 MiB for heavy workloads
    .name("r-worker".to_string())
    .spawn(|| {
        // R API calls safe here
    })?;

๐Ÿ”—Scoped Threads: scope_with_r

For borrowing from the enclosing scope:

use miniextendr_api::scope_with_r;

let data = vec![1, 2, 3];

std::thread::scope(|s| {
    scope_with_r(s, |_| {
        // Can borrow `data` here!
        println!("len: {}", data.len());
        // R API calls also safe
    });
});

Note: Scoped threads use Rustโ€™s default stack size (2 MiB). For larger stacks, use spawn_with_r.

๐Ÿ”—Manual Control: StackCheckGuard

For existing threads or fine-grained control:

use miniextendr_api::StackCheckGuard;

std::thread::spawn(|| {
    let _guard = StackCheckGuard::disable();

    // R API calls safe while guard is alive
    unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) };

    // Original limit restored when _guard drops
});

๐Ÿ”—One-Time Disable: with_stack_checking_disabled

use miniextendr_api::with_stack_checking_disabled;

let result = with_stack_checking_disabled(|| {
    unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) }
});

๐Ÿ”—Stack Size Requirements

๐Ÿ”—Automatic Configuration

When you enable the nonapi feature, miniextendr-apiโ€™s build.rs automatically sets linker flags to configure an 8 MiB stack for binaries, tests, examples, and cdylib crates:

PlatformLinker Flag
Windows MSVC/STACK:8388608
Windows GNU-Wl,--stack,8388608
macOS-Wl,-stack_size,800000
Linux/BSD-Wl,-z,stack-size=8388608

To override (e.g., for Windows Rโ€™s 64 MiB), add to .cargo/config.toml:

[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-arg=/STACK:67108864"]  # 64 MiB

๐Ÿ”—Platform Defaults

R doesnโ€™t enforce a specific stack size - it uses whatever the OS provides:

PlatformDefault Stack SizeSource
Linux~8 MiBulimit -s / getrlimit(RLIMIT_STACK)
macOS~8 MiBsysctl KERN_USRSTACK
Windows64 MiBLinker flag (since R 4.2)

Rustโ€™s default thread stack is only 2 MiB, which may be insufficient for:

  • Deep recursion (lapply chains, recursive functions)
  • Complex formulas
  • Large tryCatch stacks

๐Ÿ”—Available Constants

/// 8 MiB - conservative default, matches Unix R
/// (Always available, no feature gate required)
pub const DEFAULT_R_STACK_SIZE: usize = 8 * 1024 * 1024;

/// 64 MiB - matches Windows R for heavy workloads
/// Only available on Windows (#[cfg(windows)])
pub const WINDOWS_R_STACK_SIZE: usize = 64 * 1024 * 1024;

๐Ÿ”—Important Caveats

๐Ÿ”—R is Still Single-Threaded

Disabling stack checking allows calling R from other threads, but R itself is not thread-safe. You must ensure:

  1. No concurrent R API calls - Use mutexes or channels to serialize access
  2. GC safety - Rโ€™s garbage collector is not thread-aware
  3. Global state - R has extensive global state that isnโ€™t synchronized

Use worker threads for Rust computation, marshal R calls to main thread:

use std::sync::mpsc;

// Channel for R results
let (tx, rx) = mpsc::channel();

// Worker thread does Rust computation
spawn_with_r(move || {
    let rust_result = expensive_rust_computation();

    // Convert to R on this thread (with guard)
    let r_result = unsafe { rust_result.into_sexp() };

    tx.send(r_result).unwrap();
});

// Main thread receives R object
let sexp = rx.recv().unwrap();

๐Ÿ”—ALTREP Callbacks

ALTREP methods are called by R on the main thread, so they donโ€™t need StackCheckGuard. However, if an ALTREP method spawns threads that call back into R, those threads need the guard.

๐Ÿ”—ALTREP and Thread Safety

When R passes an ALTREP vector (e.g., 1:10) to a #[miniextendr] function, miniextendr auto-materializes it on the R main thread before the function body runs. This ensures the data pointer is stable before any SEXP could cross a thread boundary.

For explicit ALTREP handling, use AltrepSexp โ€” a !Send + !Sync wrapper that prevents un-materialized ALTREP vectors from reaching rayon or other worker threads at compile time.

See Receiving ALTREP from R for the full guide.

๐Ÿ”—Non-API Functions Used

These are gated behind feature = "nonapi" and may break with R updates:

SymbolPurpose
R_CStackStartStack top address
R_CStackLimitStack limit (set to usize::MAX to disable)
R_CStackDirStack growth direction

See NONAPI.md for the full tracking document.

๐Ÿ”—Known Limitations

  • Async/await is not supported. Rโ€™s C API is single-threaded and synchronous; use blocking I/O on the worker thread or R-level parallelism (mirai, callr). See GAPS.md.
  • Spawned-thread panics cannot be propagated through extern "C-unwind" functions. Handle thread errors explicitly via Result rather than resume_unwind. See GAPS.md.
  • Debug-only SEXP thread assertions mean release builds may not detect SEXP access from wrong threads. Checked FFI wrappers provide runtime checks in all build modes.

See GAPS.md for the full catalog of known limitations.


๐Ÿ”—See Also

  • SAFETY.md โ€“ Safety invariants and the worker thread model
  • ERROR_HANDLING.md โ€“ Panic handling and R error propagation
  • FEATURES.md โ€“ The nonapi feature flag for thread utilities
  • RAYON.md โ€“ Parallel iteration with Rayon

๐Ÿ”—References

  • R source: src/main/errors.c - R_CheckStack() implementation
  • R source: src/unix/system.c - Unix stack initialization
  • R source: src/gnuwin32/system.c - Windows stack initialization
  • R NEWS: โ€œOn Windows, the C stack size has been increased to 64MBโ€ (R 4.2)