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//! # Background
8//!
9//! R tracks three variables for stack overflow detection (all non-API):
10//! - `R_CStackStart` - top of the main thread's stack
11//! - `R_CStackLimit` - stack size limit
12//! - `R_CStackDir` - stack growth direction
13//!
14//! When R API functions check the stack, they compare the current stack pointer
15//! against these bounds. On a different thread, the stack is completely different,
16//! causing false stack overflow detection.
17//!
18//! # Solution
19//!
20//! Setting `R_CStackLimit` to `usize::MAX` disables stack checking entirely.
21//! This is safe because:
22//! 1. The OS still enforces real stack limits
23//! 2. R will still function correctly, just without its own overflow detection
24//!
25//! # Example
26//!
27//! ```ignore
28//! use miniextendr_api::thread::StackCheckGuard;
29//!
30//! std::thread::spawn(|| {
31//!     // This would segfault without the guard!
32//!     let _guard = StackCheckGuard::disable();
33//!
34//!     // Now safe to call R APIs
35//!     unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) };
36//!
37//!     // Guard restores original limit on drop
38//! });
39//! ```
40//!
41//! # Feature Gate
42//!
43//! This module requires the `nonapi` feature because it accesses non-API
44//! R internals (`R_CStackLimit`, `R_CStackStart`, `R_CStackDir`).
45
46#[cfg(feature = "nonapi")]
47use crate::ffi::nonapi_stack::{
48    get_r_cstack_dir, get_r_cstack_limit, get_r_cstack_start, set_r_cstack_limit,
49};
50
51#[cfg(feature = "nonapi")]
52use std::sync::atomic::{AtomicUsize, Ordering};
53
54/// Global refcount for active stack check guards.
55/// When count > 0, stack checking is disabled.
56#[cfg(feature = "nonapi")]
57static STACK_GUARD_COUNT: AtomicUsize = AtomicUsize::new(0);
58
59/// Original R_CStackLimit value before any guards were created.
60/// Only valid when STACK_GUARD_COUNT > 0.
61#[cfg(feature = "nonapi")]
62static ORIGINAL_STACK_LIMIT: AtomicUsize = AtomicUsize::new(0);
63
64/// RAII guard that disables R's stack checking and restores it on drop.
65///
66/// Use this when calling R APIs from a thread other than the main R thread.
67///
68/// Multiple guards can be active concurrently. Stack checking is only restored
69/// when the last guard is dropped.
70///
71/// # Example
72///
73/// ```ignore
74/// let _guard = StackCheckGuard::disable();
75/// // R API calls are now safe on this thread
76/// // Original limit restored when _guard is dropped
77/// ```
78#[cfg(feature = "nonapi")]
79pub struct StackCheckGuard {
80    // Unit struct - state is in global atomics
81    _private: (),
82}
83
84#[cfg(feature = "nonapi")]
85impl StackCheckGuard {
86    /// Disable R's stack checking and return a guard that restores it on drop.
87    ///
88    /// Multiple guards can be created concurrently (even from different threads).
89    /// Stack checking is only restored when the last guard is dropped.
90    ///
91    /// # Safety
92    ///
93    /// This is safe to call, but the caller must ensure that R has been
94    /// initialized (the R_CStackLimit variable exists).
95    #[must_use]
96    pub fn disable() -> Self {
97        // Atomically increment guard count and save original limit if we're the first
98        let prev_count = STACK_GUARD_COUNT.fetch_add(1, Ordering::SeqCst);
99        if prev_count == 0 {
100            // We're the first guard - save the original limit
101            let original = get_r_cstack_limit();
102            ORIGINAL_STACK_LIMIT.store(original, Ordering::SeqCst);
103            // Disable stack checking
104            unsafe {
105                set_r_cstack_limit(usize::MAX);
106            }
107        }
108        Self { _private: () }
109    }
110
111    /// Get the original limit value that will be restored (for debugging).
112    pub fn original_limit() -> usize {
113        ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst)
114    }
115
116    /// Get the current number of active guards (for debugging).
117    pub fn active_count() -> usize {
118        STACK_GUARD_COUNT.load(Ordering::SeqCst)
119    }
120}
121
122#[cfg(feature = "nonapi")]
123impl Drop for StackCheckGuard {
124    fn drop(&mut self) {
125        // Atomically decrement guard count and restore limit if we're the last
126        let prev_count = STACK_GUARD_COUNT.fetch_sub(1, Ordering::SeqCst);
127        if prev_count == 1 {
128            // We were the last guard - restore the original limit
129            let original = ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst);
130            unsafe {
131                set_r_cstack_limit(original);
132            }
133        }
134    }
135}
136
137/// Check if stack checking is currently disabled.
138///
139/// Returns `true` if `R_CStackLimit` is set to `usize::MAX`.
140#[cfg(feature = "nonapi")]
141pub fn is_stack_checking_disabled() -> bool {
142    get_r_cstack_limit() == usize::MAX
143}
144
145/// Get the current stack checking configuration (for debugging).
146///
147/// Returns `(start, limit, direction)`.
148#[cfg(feature = "nonapi")]
149pub fn get_stack_config() -> (usize, usize, i32) {
150    (
151        get_r_cstack_start(),
152        get_r_cstack_limit(),
153        get_r_cstack_dir(),
154    )
155}
156
157/// Disable stack checking permanently for the current session.
158///
159/// Unlike [`StackCheckGuard`], this does not restore the original value.
160/// Use this at program startup if you know you'll be calling R from multiple threads.
161///
162/// # Safety
163///
164/// Safe to call, but should only be called once during initialization.
165#[cfg(feature = "nonapi")]
166pub fn disable_stack_checking_permanently() {
167    unsafe {
168        set_r_cstack_limit(usize::MAX);
169    }
170}
171
172/// Execute a closure with stack checking disabled.
173///
174/// This is a convenience wrapper around [`StackCheckGuard`].
175///
176/// # Example
177///
178/// ```ignore
179/// let result = with_stack_checking_disabled(|| {
180///     unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) }
181/// });
182/// ```
183#[cfg(feature = "nonapi")]
184pub fn with_stack_checking_disabled<F, R>(f: F) -> R
185where
186    F: FnOnce() -> R,
187{
188    let _guard = StackCheckGuard::disable();
189    f()
190}
191
192// region: Thread spawning with R-compatible settings
193
194/// Default stack size for R-compatible threads (8 MiB).
195///
196/// R doesn't enforce a specific stack size - it uses whatever the OS provides:
197/// - **Unix**: Typically 8 MiB from `ulimit -s`
198/// - **Windows**: 64 MiB for the main thread (since R 4.2)
199///
200/// Since we disable R's stack checking via `StackCheckGuard`, the size is about
201/// practical needs rather than R enforcement. Deep recursion in R code (especially
202/// recursive functions, `lapply` chains, or complex formulas) can use significant stack.
203///
204/// Rust's default thread stack is only 2 MiB, which may be insufficient for deep R calls.
205/// We default to 8 MiB as a reasonable balance. Increase via [`RThreadBuilder::stack_size`]
206/// if you encounter stack overflows.
207pub const DEFAULT_R_STACK_SIZE: usize = 8 * 1024 * 1024;
208
209/// Stack size matching Windows R (64 MiB).
210///
211/// Use this if your code involves very deep recursion or complex R operations.
212/// Windows R uses 64 MiB for its main thread since R 4.2.
213#[cfg(windows)]
214pub const WINDOWS_R_STACK_SIZE: usize = 64 * 1024 * 1024;
215
216/// Spawn a new thread configured for calling R APIs.
217///
218/// This function:
219/// 1. Sets a stack size appropriate for R (8 MiB by default)
220/// 2. Automatically disables R's stack checking via `StackCheckGuard`
221/// 3. Restores stack checking when the thread completes
222///
223/// # Example
224///
225/// ```ignore
226/// use miniextendr_api::thread::spawn_with_r;
227///
228/// let handle = spawn_with_r(|| {
229///     // Safe to call R APIs here!
230///     let result = unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) };
231///     result
232/// })?;
233///
234/// let sexp = handle.join().unwrap();
235/// ```
236///
237/// # Panics
238///
239/// Returns an error if the thread cannot be spawned (e.g., resource exhaustion).
240#[cfg(feature = "nonapi")]
241pub fn spawn_with_r<F, T>(f: F) -> std::io::Result<std::thread::JoinHandle<T>>
242where
243    F: FnOnce() -> T + Send + 'static,
244    T: Send + 'static,
245{
246    RThreadBuilder::new().spawn(f)
247}
248
249/// Builder for spawning threads with R-appropriate stack sizes.
250///
251/// This builder is always available and configures threads with stack sizes
252/// suitable for R workloads (8 MiB default, vs Rust's 2 MiB default).
253///
254/// When the `nonapi` feature is enabled, spawned threads also automatically
255/// disable R's stack checking via `StackCheckGuard`, allowing R API calls
256/// from the thread.
257///
258/// # Example
259///
260/// ```ignore
261/// use miniextendr_api::thread::RThreadBuilder;
262///
263/// let handle = RThreadBuilder::new()
264///     .stack_size(16 * 1024 * 1024)  // 16 MiB
265///     .name("r-worker".to_string())
266///     .spawn(|| {
267///         // With `nonapi`: R API calls safe here
268///         // Without `nonapi`: Just a thread with correct stack size
269///     })?;
270/// ```
271pub struct RThreadBuilder {
272    stack_size: usize,
273    name: Option<String>,
274}
275
276impl Default for RThreadBuilder {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282impl RThreadBuilder {
283    /// Create a new builder with default settings.
284    ///
285    /// Default stack size is [`DEFAULT_R_STACK_SIZE`] (8 MiB).
286    #[must_use]
287    pub fn new() -> Self {
288        Self {
289            stack_size: DEFAULT_R_STACK_SIZE,
290            name: None,
291        }
292    }
293
294    /// Set the stack size for the thread.
295    ///
296    /// R typically requires more stack space than Rust's default 2 MiB.
297    /// The default is 8 MiB to match typical R installations.
298    #[must_use]
299    pub fn stack_size(mut self, size: usize) -> Self {
300        self.stack_size = size;
301        self
302    }
303
304    /// Set the name for the thread (for debugging).
305    #[must_use]
306    pub fn name(mut self, name: String) -> Self {
307        self.name = Some(name);
308        self
309    }
310
311    /// Spawn the thread with the configured settings.
312    ///
313    /// With `nonapi` feature: automatically disables R's stack checking.
314    /// Without `nonapi` feature: just spawns with the configured stack size.
315    pub fn spawn<F, T>(self, f: F) -> std::io::Result<std::thread::JoinHandle<T>>
316    where
317        F: FnOnce() -> T + Send + 'static,
318        T: Send + 'static,
319    {
320        let mut builder = std::thread::Builder::new().stack_size(self.stack_size);
321
322        if let Some(name) = self.name {
323            builder = builder.name(name);
324        }
325
326        #[cfg(feature = "nonapi")]
327        {
328            builder.spawn(move || {
329                let _guard = StackCheckGuard::disable();
330                f()
331            })
332        }
333
334        #[cfg(not(feature = "nonapi"))]
335        {
336            builder.spawn(f)
337        }
338    }
339
340    /// Spawn and immediately join, returning the result.
341    ///
342    /// Convenience method for synchronous R calls on a separate thread.
343    ///
344    /// # Example
345    ///
346    /// ```ignore
347    /// let result = RThreadBuilder::new()
348    ///     .spawn_join(|| unsafe { miniextendr_api::ffi::Rf_ScalarInteger_unchecked(42) })
349    ///     .unwrap();
350    /// ```
351    pub fn spawn_join<F, T>(self, f: F) -> std::thread::Result<T>
352    where
353        F: FnOnce() -> T + Send + 'static,
354        T: Send + 'static,
355    {
356        self.spawn(f)
357            .map_err(|e| Box::new(e) as Box<dyn std::any::Any + Send>)?
358            .join()
359    }
360}
361
362/// Spawn a scoped thread configured for calling R APIs.
363///
364/// Like [`spawn_with_r`] but uses scoped threads, allowing the closure to
365/// borrow from the enclosing scope.
366///
367/// # Example
368///
369/// ```ignore
370/// use miniextendr_api::thread::scope_with_r;
371///
372/// let data = vec![1, 2, 3];
373///
374/// std::thread::scope(|s| {
375///     scope_with_r(s, |_| {
376///         // Can borrow `data` here!
377///         println!("data len: {}", data.len());
378///         // R API calls also safe
379///     });
380/// });
381/// ```
382#[cfg(feature = "nonapi")]
383pub fn scope_with_r<'scope, 'env, F, T>(
384    scope: &'scope std::thread::Scope<'scope, 'env>,
385    f: F,
386) -> std::thread::ScopedJoinHandle<'scope, T>
387where
388    F: FnOnce(&'scope std::thread::Scope<'scope, 'env>) -> T + Send + 'scope,
389    T: Send + 'scope,
390{
391    // Note: scoped threads don't support custom stack sizes in std
392    // This is a known limitation. For custom stack sizes, use spawn_with_r.
393    scope.spawn(move || {
394        let _guard = StackCheckGuard::disable();
395        f(scope)
396    })
397}
398
399#[cfg(test)]
400#[cfg(feature = "nonapi")]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_guard_saves_and_restores() {
406        let original = get_r_cstack_limit();
407
408        {
409            let _guard = StackCheckGuard::disable();
410            // The original limit is saved in ORIGINAL_STACK_LIMIT
411            assert_eq!(ORIGINAL_STACK_LIMIT.load(Ordering::SeqCst), original);
412            assert!(is_stack_checking_disabled());
413        }
414
415        // After guard drops, should be restored
416        assert_eq!(get_r_cstack_limit(), original);
417    }
418}
419// endregion