Skip to main content

miniextendr_api/
thread.rs

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