miniextendr_api/thread.rs
1//! Thread safety utilities for calling R from non-main threads.
2//!
3//! R's stack checking mechanism causes segfaults when R API functions are called
4//! from threads other than the main R thread. This module provides utilities to
5//! safely disable stack checking when crossing thread boundaries.
6//!
7//! # Prefer [`crate::worker::with_r_thread`] in normal code
8//!
9//! This module is the **escape hatch** for advanced cases (rayon pools,
10//! externally-spawned threads that need to touch R briefly). The default
11//! tool for crossing back to R is [`crate::worker::with_r_thread`], which
12//! routes the closure to the recorded main thread instead of unsafely
13//! patching R's internal stack bounds.
14//!
15//! `StackCheckGuard` is gated behind the `nonapi` feature because it
16//! mutates `R_CStackStart` / `R_CStackLimit` / `R_CStackDir`, none of which
17//! are part of R's public C API. The lint **MXL301** treats this guard the
18//! same way as the other known-safe contexts — inside a `StackCheckGuard`
19//! scope you may use `*_unchecked` FFI variants because the main-thread
20//! assertion would be wrong (you're explicitly *not* on the main thread).
21//!
22//! # Don't use `Rf_error` here either
23//!
24//! A longjmp from a non-main thread is undefined behaviour even with the
25//! stack check disabled. Panic, capture the message in your guard's
26//! fallback (see [`crate::ffi_guard::guarded_ffi_call_with_fallback`]), and
27//! surface the failure to the main thread before letting R see it. The lint
28//! **MXL300** rejects direct `Rf_error` calls in user code.
29//!
30//! # Cross references
31//!
32//! - [`crate::worker::with_r_thread`] — preferred path for crossing back to R.
33//! - [`crate::sys`] — checked vs `*_unchecked` FFI surface.
34//!
35//! # Background
36//!
37//! R tracks three variables for stack overflow detection (all non-API):
38//! - `R_CStackStart` - top of the main thread's stack
39//! - `R_CStackLimit` - stack size limit
40//! - `R_CStackDir` - stack growth direction
41//!
42//! When R API functions check the stack, they compare the current stack pointer
43//! against these bounds. On a different thread, the stack is completely different,
44//! causing false stack overflow detection.
45//!
46//! # Solution
47//!
48//! Setting `R_CStackLimit` to `usize::MAX` disables stack checking entirely.
49//! This is safe because:
50//! 1. The OS still enforces real stack limits
51//! 2. R will still function correctly, just without its own overflow detection
52//!
53//! # Example
54//!
55//! ```ignore
56//! use miniextendr_api::thread::StackCheckGuard;
57//!
58//! std::thread::spawn(|| {
59//! // This would segfault without the guard!
60//! let _guard = StackCheckGuard::disable();
61//!
62//! // Now safe to call R APIs
63//! unsafe { miniextendr_api::SEXP::scalar_integer_unchecked(42) };
64//!
65//! // Guard restores original limit on drop
66//! });
67//! ```
68//!
69//! # Feature Gate
70//!
71//! This module requires the `nonapi` feature because it accesses non-API
72//! R internals (`R_CStackLimit`, `R_CStackStart`, `R_CStackDir`).
73
74#[cfg(feature = "nonapi")]
75use crate::sys::nonapi_stack::{
76 get_r_cstack_dir, get_r_cstack_limit, get_r_cstack_start, set_r_cstack_limit,
77};
78
79#[cfg(feature = "nonapi")]
80use std::sync::atomic::{AtomicUsize, Ordering};
81
82/// Global refcount for active stack check guards.
83/// When count > 0, stack checking is disabled.
84#[cfg(feature = "nonapi")]
85static STACK_GUARD_COUNT: AtomicUsize = AtomicUsize::new(0);
86
87/// Original R_CStackLimit value before any guards were created.
88/// Only valid when STACK_GUARD_COUNT > 0.
89#[cfg(feature = "nonapi")]
90static ORIGINAL_STACK_LIMIT: AtomicUsize = AtomicUsize::new(0);
91
92/// RAII guard that disables R's stack checking and restores it on drop.
93///
94/// Use this when calling R APIs from a thread other than the main R thread.
95///
96/// Multiple guards can be active concurrently. Stack checking is only restored
97/// when the last guard is dropped.
98///
99/// # Example
100///
101/// ```ignore
102/// let _guard = StackCheckGuard::disable();
103/// // R API calls are now safe on this thread
104/// // Original limit restored when _guard is dropped
105/// ```
106#[cfg(feature = "nonapi")]
107pub struct StackCheckGuard {
108 // Unit struct - state is in global atomics
109 _private: (),
110}
111
112#[cfg(feature = "nonapi")]
113impl StackCheckGuard {
114 /// Disable R's stack checking and return a guard that restores it on drop.
115 ///
116 /// Multiple guards can be created concurrently (even from different threads).
117 /// Stack checking is only restored when the last guard is dropped.
118 ///
119 /// # Safety
120 ///
121 /// This is safe to call, but the caller must ensure that R has been
122 /// initialized (the R_CStackLimit variable exists).
123 #[must_use]
124 pub fn disable() -> Self {
125 // Atomically increment guard count and save original limit if we're the first
126 let prev_count = STACK_GUARD_COUNT.fetch_add(1, Ordering::SeqCst);
127 if prev_count == 0 {
128 // We're the first guard - save the original limit
129 let original = get_r_cstack_limit();
130 ORIGINAL_STACK_LIMIT.store(original, Ordering::SeqCst);
131 // Disable stack checking
132 unsafe {
133 set_r_cstack_limit(usize::MAX);
134 }
135 }
136 Self { _private: () }
137 }
138
139 /// Get the original limit value that will be restored (for debugging).
140 pub fn original_limit() -> usize {
141 ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst)
142 }
143
144 /// Get the current number of active guards (for debugging).
145 pub fn active_count() -> usize {
146 STACK_GUARD_COUNT.load(Ordering::SeqCst)
147 }
148}
149
150#[cfg(feature = "nonapi")]
151impl Drop for StackCheckGuard {
152 fn drop(&mut self) {
153 // Atomically decrement guard count and restore limit if we're the last
154 let prev_count = STACK_GUARD_COUNT.fetch_sub(1, Ordering::SeqCst);
155 if prev_count == 1 {
156 // We were the last guard - restore the original limit
157 let original = ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst);
158 unsafe {
159 set_r_cstack_limit(original);
160 }
161 }
162 }
163}
164
165/// Check if stack checking is currently disabled.
166///
167/// Returns `true` if `R_CStackLimit` is set to `usize::MAX`.
168#[cfg(feature = "nonapi")]
169pub fn is_stack_checking_disabled() -> bool {
170 get_r_cstack_limit() == usize::MAX
171}
172
173/// Get the current stack checking configuration (for debugging).
174///
175/// Returns `(start, limit, direction)`.
176#[cfg(feature = "nonapi")]
177pub fn get_stack_config() -> (usize, usize, i32) {
178 (
179 get_r_cstack_start(),
180 get_r_cstack_limit(),
181 get_r_cstack_dir(),
182 )
183}
184
185/// Disable stack checking permanently for the current session.
186///
187/// Unlike [`StackCheckGuard`], this does not restore the original value.
188/// Use this at program startup if you know you'll be calling R from multiple threads.
189///
190/// # Safety
191///
192/// Safe to call, but should only be called once during initialization.
193#[cfg(feature = "nonapi")]
194pub fn disable_stack_checking_permanently() {
195 unsafe {
196 set_r_cstack_limit(usize::MAX);
197 }
198 // Pin the saved limit to `usize::MAX` so a later `StackCheckGuard` drop can't
199 // silently re-enable checking. If a guard is already active, `ORIGINAL_STACK_LIMIT`
200 // holds the real pre-disable limit, and the last guard's drop would restore it —
201 // undoing the "permanent" disable. Overwriting it with `usize::MAX` makes that
202 // restore a no-op, so the disable stays durable regardless of guard ordering.
203 ORIGINAL_STACK_LIMIT.store(usize::MAX, Ordering::SeqCst);
204}
205
206/// Execute a closure with stack checking disabled.
207///
208/// This is a convenience wrapper around [`StackCheckGuard`].
209///
210/// # Example
211///
212/// ```ignore
213/// let result = with_stack_checking_disabled(|| {
214/// unsafe { miniextendr_api::SEXP::scalar_integer_unchecked(42) }
215/// });
216/// ```
217#[cfg(feature = "nonapi")]
218pub fn with_stack_checking_disabled<F, R>(f: F) -> R
219where
220 F: FnOnce() -> R,
221{
222 let _guard = StackCheckGuard::disable();
223 f()
224}
225
226// region: Thread spawning with R-compatible settings
227
228/// Default stack size for R-compatible threads (8 MiB).
229///
230/// R doesn't enforce a specific stack size - it uses whatever the OS provides:
231/// - **Unix**: Typically 8 MiB from `ulimit -s`
232/// - **Windows**: 64 MiB for the main thread (since R 4.2)
233///
234/// Since we disable R's stack checking via `StackCheckGuard`, the size is about
235/// practical needs rather than R enforcement. Deep recursion in R code (especially
236/// recursive functions, `lapply` chains, or complex formulas) can use significant stack.
237///
238/// Rust's default thread stack is only 2 MiB, which may be insufficient for deep R calls.
239/// We default to 8 MiB as a reasonable balance. Increase via [`RThreadBuilder::stack_size`]
240/// if you encounter stack overflows.
241pub const DEFAULT_R_STACK_SIZE: usize = 8 * 1024 * 1024;
242
243/// Stack size matching Windows R (64 MiB).
244///
245/// Use this if your code involves very deep recursion or complex R operations.
246/// Windows R uses 64 MiB for its main thread since R 4.2.
247///
248/// # Why larger than [`DEFAULT_R_STACK_SIZE`]
249///
250/// This is 8x the [`DEFAULT_R_STACK_SIZE`] used on other platforms. Two factors
251/// motivate the larger reservation on Windows:
252///
253/// - Newly spawned Windows threads get a comparatively small *committed* stack by
254/// default, and the OS does not grow a thread stack the way `ulimit -s` allows
255/// on typical Unix configurations — so the size we ask for up front is closer to
256/// the size we actually get.
257/// - R's own choice of 64 MiB for its Windows main thread (since R 4.2) reflects
258/// that deep R evaluation consumes more C stack on this platform than the
259/// ~8 MiB Unix default comfortably covers.
260///
261/// Matching R's main-thread reservation here keeps deep R evaluation on a spawned
262/// thread from overflowing the stack. This is a conservative reservation, not a
263/// precise measurement of any single call's stack frame.
264#[cfg(windows)]
265pub const WINDOWS_R_STACK_SIZE: usize = 64 * 1024 * 1024;
266
267/// Spawn a new thread configured for calling R APIs.
268///
269/// This function:
270/// 1. Sets a stack size appropriate for R (8 MiB by default)
271/// 2. Automatically disables R's stack checking via `StackCheckGuard`
272/// 3. Restores stack checking when the thread completes
273///
274/// # Example
275///
276/// ```ignore
277/// use miniextendr_api::thread::spawn_with_r;
278///
279/// let handle = spawn_with_r(|| {
280/// // Safe to call R APIs here!
281/// let result = unsafe { miniextendr_api::SEXP::scalar_integer_unchecked(42) };
282/// result
283/// })?;
284///
285/// let sexp = handle.join().unwrap();
286/// ```
287///
288/// # Panics
289///
290/// Returns an error if the thread cannot be spawned (e.g., resource exhaustion).
291#[cfg(feature = "nonapi")]
292pub fn spawn_with_r<F, T>(f: F) -> std::io::Result<std::thread::JoinHandle<T>>
293where
294 F: FnOnce() -> T + Send + 'static,
295 T: Send + 'static,
296{
297 RThreadBuilder::new().spawn(f)
298}
299
300/// Builder for spawning threads with R-appropriate stack sizes.
301///
302/// This builder is always available and configures threads with stack sizes
303/// suitable for R workloads (8 MiB default, vs Rust's 2 MiB default).
304///
305/// When the `nonapi` feature is enabled, spawned threads also automatically
306/// disable R's stack checking via `StackCheckGuard`, allowing R API calls
307/// from the thread.
308///
309/// # Example
310///
311/// ```ignore
312/// use miniextendr_api::thread::RThreadBuilder;
313///
314/// let handle = RThreadBuilder::new()
315/// .stack_size(16 * 1024 * 1024) // 16 MiB
316/// .name("r-worker".to_string())
317/// .spawn(|| {
318/// // With `nonapi`: R API calls safe here
319/// // Without `nonapi`: Just a thread with correct stack size
320/// })?;
321/// ```
322pub struct RThreadBuilder {
323 stack_size: usize,
324 name: Option<String>,
325}
326
327impl Default for RThreadBuilder {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl RThreadBuilder {
334 /// Create a new builder with default settings.
335 ///
336 /// Default stack size is [`DEFAULT_R_STACK_SIZE`] (8 MiB).
337 #[must_use]
338 pub fn new() -> Self {
339 Self {
340 stack_size: DEFAULT_R_STACK_SIZE,
341 name: None,
342 }
343 }
344
345 /// Set the stack size for the thread.
346 ///
347 /// R typically requires more stack space than Rust's default 2 MiB.
348 /// The default is 8 MiB to match typical R installations.
349 #[must_use]
350 pub fn stack_size(mut self, size: usize) -> Self {
351 self.stack_size = size;
352 self
353 }
354
355 /// Set the name for the thread (for debugging).
356 #[must_use]
357 pub fn name(mut self, name: String) -> Self {
358 self.name = Some(name);
359 self
360 }
361
362 /// Spawn the thread with the configured settings.
363 ///
364 /// With `nonapi` feature: automatically disables R's stack checking.
365 /// Without `nonapi` feature: just spawns with the configured stack size.
366 pub fn spawn<F, T>(self, f: F) -> std::io::Result<std::thread::JoinHandle<T>>
367 where
368 F: FnOnce() -> T + Send + 'static,
369 T: Send + 'static,
370 {
371 let mut builder = std::thread::Builder::new().stack_size(self.stack_size);
372
373 if let Some(name) = self.name {
374 builder = builder.name(name);
375 }
376
377 #[cfg(feature = "nonapi")]
378 {
379 builder.spawn(move || {
380 let _guard = StackCheckGuard::disable();
381 f()
382 })
383 }
384
385 #[cfg(not(feature = "nonapi"))]
386 {
387 builder.spawn(f)
388 }
389 }
390
391 /// Spawn and immediately join, returning the result.
392 ///
393 /// Convenience method for synchronous R calls on a separate thread.
394 ///
395 /// # Example
396 ///
397 /// ```ignore
398 /// let result = RThreadBuilder::new()
399 /// .spawn_join(|| unsafe { miniextendr_api::SEXP::scalar_integer_unchecked(42) })
400 /// .unwrap();
401 /// ```
402 pub fn spawn_join<F, T>(self, f: F) -> std::thread::Result<T>
403 where
404 F: FnOnce() -> T + Send + 'static,
405 T: Send + 'static,
406 {
407 self.spawn(f)
408 .map_err(|e| Box::new(e) as Box<dyn std::any::Any + Send>)?
409 .join()
410 }
411}
412
413/// Spawn a scoped thread configured for calling R APIs.
414///
415/// Like [`spawn_with_r`] but uses scoped threads, allowing the closure to
416/// borrow from the enclosing scope.
417///
418/// # Example
419///
420/// ```ignore
421/// use miniextendr_api::thread::scope_with_r;
422///
423/// let data = vec![1, 2, 3];
424///
425/// std::thread::scope(|s| {
426/// scope_with_r(s, |_| {
427/// // Can borrow `data` here!
428/// println!("data len: {}", data.len());
429/// // R API calls also safe
430/// });
431/// });
432/// ```
433#[cfg(feature = "nonapi")]
434pub fn scope_with_r<'scope, 'env, F, T>(
435 scope: &'scope std::thread::Scope<'scope, 'env>,
436 f: F,
437) -> std::thread::ScopedJoinHandle<'scope, T>
438where
439 F: FnOnce(&'scope std::thread::Scope<'scope, 'env>) -> T + Send + 'scope,
440 T: Send + 'scope,
441{
442 // Note: scoped threads don't support custom stack sizes in std
443 // This is a known limitation. For custom stack sizes, use spawn_with_r.
444 scope.spawn(move || {
445 let _guard = StackCheckGuard::disable();
446 f(scope)
447 })
448}
449
450#[cfg(test)]
451#[cfg(feature = "nonapi")]
452mod tests {
453 use super::*;
454 use std::sync::Mutex;
455
456 // These tests mutate process-global state (`R_CStackLimit`, `STACK_GUARD_COUNT`,
457 // `ORIGINAL_STACK_LIMIT`), so they must not run concurrently. Serialize them.
458 static STACK_TEST_LOCK: Mutex<()> = Mutex::new(());
459
460 #[test]
461 fn test_guard_saves_and_restores() {
462 let _serial = STACK_TEST_LOCK.lock().unwrap();
463 let original = get_r_cstack_limit();
464
465 {
466 let _guard = StackCheckGuard::disable();
467 // The original limit is saved in ORIGINAL_STACK_LIMIT
468 assert_eq!(ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst), original);
469 assert!(is_stack_checking_disabled());
470 }
471
472 // After guard drops, should be restored
473 assert_eq!(get_r_cstack_limit(), original);
474 }
475
476 /// Regression test for the "permanent disable silently undone by a guard drop" bug.
477 ///
478 /// Ordering (a): a `StackCheckGuard` is active (so `ORIGINAL_STACK_LIMIT` holds the
479 /// real pre-disable limit) when `disable_stack_checking_permanently()` is called.
480 /// Before the fix, the guard's `Drop` restored that saved limit, re-enabling stack
481 /// checking and undoing the "permanent" disable. After the fix,
482 /// `disable_stack_checking_permanently()` also pins `ORIGINAL_STACK_LIMIT` to
483 /// `usize::MAX`, so the drop's restore is a no-op and the limit stays at `usize::MAX`.
484 #[test]
485 fn test_permanent_disable_survives_guard_drop() {
486 let _serial = STACK_TEST_LOCK.lock().unwrap();
487 let original = get_r_cstack_limit();
488
489 {
490 // Guard active: ORIGINAL_STACK_LIMIT now holds the real pre-disable limit.
491 let _guard = StackCheckGuard::disable();
492 assert_eq!(ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst), original);
493
494 // Permanent disable while the guard is still alive.
495 disable_stack_checking_permanently();
496 // The saved limit is pinned to MAX so a later restore can't re-enable checking.
497 assert_eq!(ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst), usize::MAX);
498 }
499 // Guard dropped: limit must remain disabled, not bounce back to `original`.
500 assert!(is_stack_checking_disabled());
501 assert_eq!(get_r_cstack_limit(), usize::MAX);
502
503 // Restore the real limit so we don't leak the permanent disable into other tests.
504 unsafe {
505 set_r_cstack_limit(original);
506 }
507 ORIGINAL_STACK_LIMIT.store(0, Ordering::SeqCst);
508 }
509}
510// endregion