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