Expand description
Worker thread infrastructure for safe Rust-R FFI.
§Why a worker thread at all?
R’s error handling uses longjmp, which skips Rust destructors and leaks
resources. Generated wrappers therefore establish a destructor-safe error
boundary. The default boundary runs inline on R’s main thread inside
R_UnwindProtect; #[miniextendr(worker)] (or the worker-default
feature) instead dispatches the Rust body to a dedicated worker.
On the worker-dispatch path, user code is off the R main thread.
Merely enabling the worker-thread infrastructure feature does not select
that path. Anything in an opted-in worker body that calls R’s C API
(allocating SEXPs, walking attributes, accessing INTEGER(x)) must cross
back to main via with_r_thread.
§Public API
with_r_thread— Execute a closure on R’s main thread. This is the bridge: call it from inside a#[miniextendr]body whenever you need to touch the R FFI.is_r_main_thread— Check if the current thread is R’s main thread.Sendable—#[doc(hidden)]wrapper used by the macros to ferrySEXP(and other!Sendtypes) across the worker channel. The author asserts the value is only consumed on the main thread.
§Tradeoffs
- Default to checked FFI variants (
Rf_allocVector,INTEGER, …) so an active worker call routes correctly and an arbitrary off-thread call fails instead of reaching R. - Inside a
with_r_threadbody, the check is redundant — the*_uncheckedvariants incrate::sysare safe to call there (recognised by the lint MXL301, alongside ALTREP callbacks andcrate::unwind_protect::with_r_unwind_protectbodies). - Don’t raise R errors directly from worker-thread code.
Rf_errorwould longjmp through Rust frames on the wrong thread. Panic instead; the framework converts the panic into a structured R condition (seecrate::error_value). The lint MXL300 enforces this.
§Feature gate: worker-thread
Without the worker-thread cargo feature, all calls execute inline on
R’s main thread:
with_r_thread(f)runsf()directly (panics if not on main thread)run_on_worker(f)runsf()directly, returnsOk(f())
With the feature enabled, a dedicated worker thread is spawned at init time.
with_r_thread routes calls from the worker back to main, and run_on_worker
dispatches closures to the worker with bidirectional communication. The
worker has a 16 MB stack — keep proptest! invocations on it modest (see
the project CLAUDE.md).
§Initialization
miniextendr_runtime_init must be called from R’s main thread before any
R FFI APIs. Typically done in R_init_<pkgname>().
§Cross references
crate::unwind_protect::with_r_unwind_protect— catch R errors with Rust cleanup; sibling towith_r_thread.crate::sys— checked vs*_uncheckedFFI surface.crate::ffi_guard— guard taxonomy across boundaries.
Structs§
- Sendable 👻
- Wrapper to mark values as Send for main-thread routing.
Statics§
Functions§
- assert_
runtime_ 🔒initialized - Panic if the runtime hasn’t been initialized.
- has_
worker_ 🔒context - Check whether the current thread has a worker routing context.
- is_
r_ main_ thread - Check if the current thread is R’s main thread.
- miniextendr_
runtime_ 👻init - Initialize the miniextendr runtime.
- miniextendr_
runtime_ 👻shutdown - Shut down the miniextendr worker thread synchronously.
- panic_
message_ 👻to_ r_ error - Raise an R error from a panic message. Does not return.
- run_
on_ 👻worker - Run a closure on the worker thread with proper cleanup on panic.
- with_
r_ thread - Execute a closure on R’s main thread, returning the result.