Skip to main content

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