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