Reference page
Thread Safety in miniextendr
This document explains how to safely call R APIs from threads other than the main R thread.
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, orKERN_USRSTACK - Windows: Uses
VirtualQueryto 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:
usageto 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:
- The OS still enforces real stack limits
- 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:
- Sets stack size to 8 MiB (configurable)
- Automatically disables Rโs stack checking
- 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:
| Platform | Linker 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:
| Platform | Default Stack Size | Source |
|---|---|---|
| Linux | ~8 MiB | ulimit -s / getrlimit(RLIMIT_STACK) |
| macOS | ~8 MiB | sysctl KERN_USRSTACK |
| Windows | 64 MiB | Linker flag (since R 4.2) |
Rustโs default thread stack is only 2 MiB, which may be insufficient for:
- Deep recursion (
lapplychains, recursive functions) - Complex formulas
- Large
tryCatchstacks
๐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:
- No concurrent R API calls - Use mutexes or channels to serialize access
- GC safety - Rโs garbage collector is not thread-aware
- Global state - R has extensive global state that isnโt synchronized
๐Recommended Pattern
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:
| Symbol | Purpose |
|---|---|
R_CStackStart | Stack top address |
R_CStackLimit | Stack limit (set to usize::MAX to disable) |
R_CStackDir | Stack 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 viaResultrather thanresume_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
nonapifeature 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)