Skip to main content

miniextendr_engine/
lib.rs

1//! miniextendr-engine: standalone R embedding for Rust binaries and tests.
2//!
3//! This crate centralizes `libR` linking (via `build.rs`), R initialization, and
4//! a minimal runtime handle. It is intended for Rust-only executables and
5//! integration tests that embed R.
6//!
7//! **Not for R packages:** this crate uses non-API R internals
8//! (`Rembedded.h`, `Rinterface.h`). For R packages, depend on `miniextendr-api`
9//! and keep `nonapi` disabled.
10//!
11//! ## When to use
12//! - Rust binaries that embed R.
13//! - Integration tests or benchmarks that need full control over R startup.
14//!
15//! ## Quick start
16//!
17//! ```ignore
18//! // SAFETY: Must be called once, from the main thread.
19//! let engine = unsafe {
20//!     miniextendr_engine::REngine::build()
21//!         .with_args(&["R", "--quiet", "--vanilla"])
22//!         .init()
23//!         .expect("Failed to initialize R")
24//! };
25//!
26//! // ... use R APIs from the main thread ...
27//!
28//! std::mem::forget(engine); // optional: intentionally leak the handle
29//! ```
30//!
31//! ## Initialization details
32//! - Ensures `R_HOME` (via `R RHOME`) if missing.
33//! - Calls `Rf_initialize_R` directly to avoid double `setup_Rmainloop()`.
34//! - Calls `setup_Rmainloop()` exactly once after initialization.
35//!
36//! ## Runtime sentinel
37//!
38//! ```ignore
39//! if miniextendr_engine::r_initialized_sentinel() {
40//!     // R has been initialized in this process.
41//! }
42//! ```
43//!
44//! ## Safety
45//!
46//! - Must only be initialized once per process.
47//! - Must be called from the main thread.
48//! - No shutdown: `Rf_endEmbeddedR` is intentionally not called because the
49//!   cleanup path is not reentrant-safe. The OS reclaims resources on exit.
50
51use std::ffi::CString;
52use std::os::raw::{c_char, c_int};
53use std::process::Command;
54
55// Note: This entire crate uses non-API R functions (Rembedded.h, Rinterface.h)
56// for embedding R. It is not intended for use in R packages.
57unsafe extern "C" {
58    // R initialization (from Rembedded.h - non-API)
59    fn Rf_initialize_R(argc: c_int, argv: *mut *mut c_char) -> c_int;
60    #[allow(dead_code)]
61    fn Rf_endEmbeddedR(fatal: c_int);
62
63    // Setup functions
64    fn setup_Rmainloop();
65
66    // Global state from Rinterface.h (non-API)
67    // Use UnsafeCell for interior mutability without static mut
68    static R_Interactive: std::cell::UnsafeCell<c_int>;
69    static R_SignalHandlers: std::cell::UnsafeCell<c_int>;
70    static R_CStackStart: usize;
71    static R_CStackDir: c_int;
72    static mut R_CStackLimit: usize;
73}
74
75/// Write to R's global `R_Interactive` flag.
76///
77/// # Safety
78/// Must be called from the main thread during R initialization.
79#[inline]
80unsafe fn set_r_interactive(value: c_int) {
81    unsafe {
82        *R_Interactive.get() = value;
83    }
84}
85
86/// Write to R's global `R_SignalHandlers` flag.
87///
88/// # Safety
89/// Must be called from the main thread during R initialization.
90#[inline]
91unsafe fn set_r_signal_handlers(value: c_int) {
92    unsafe {
93        *R_SignalHandlers.get() = value;
94    }
95}
96
97/// Check whether `Rf_initialize_R` has run by inspecting stack sentinels.
98///
99/// `R_CStackStart`/`R_CStackDir` are set during R initialization on the main
100/// thread. A zero or `usize::MAX` value indicates "not initialized".
101#[inline]
102pub fn r_initialized_sentinel() -> bool {
103    unsafe {
104        let start = R_CStackStart;
105        let dir = R_CStackDir;
106        dir != 0 && start != 0 && start != usize::MAX
107    }
108}
109
110/// Builder for configuring and initializing the R runtime.
111///
112/// # Example
113///
114/// ```ignore
115/// let engine = REngine::build()
116///     .with_args(&["R", "--quiet", "--no-save"])
117///     .interactive(false)
118///     .signal_handlers(false)
119///     .init()?;
120/// ```
121pub struct REngineBuilder {
122    args: Vec<String>,
123    interactive: bool,
124    signal_handlers: bool,
125}
126
127impl Default for REngineBuilder {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl REngineBuilder {
134    /// Create a new R engine builder with default settings.
135    pub fn new() -> Self {
136        Self {
137            // Default to a non-interactive-safe setup: R requires an explicit
138            // save/no-save choice when not running interactively.
139            args: vec![
140                "R".to_string(),
141                "--quiet".to_string(),
142                "--vanilla".to_string(),
143            ],
144            interactive: false,
145            signal_handlers: false,
146        }
147    }
148
149    /// Set the command-line arguments for R initialization.
150    ///
151    /// Default is `["R", "--quiet", "--vanilla"]`.
152    pub fn with_args(mut self, args: &[&str]) -> Self {
153        self.args = args.iter().map(|s| s.to_string()).collect();
154        self
155    }
156
157    /// Set whether R should run in interactive mode.
158    ///
159    /// Default is `false`.
160    pub fn interactive(mut self, interactive: bool) -> Self {
161        self.interactive = interactive;
162        self
163    }
164
165    /// Set whether R should install signal handlers.
166    ///
167    /// Default is `false`. Set to `true` if you want R to handle Ctrl+C etc.
168    pub fn signal_handlers(mut self, enable: bool) -> Self {
169        self.signal_handlers = enable;
170        self
171    }
172
173    /// Initialize the R runtime with the configured settings.
174    ///
175    /// # Safety
176    ///
177    /// - Must only be called once per process
178    /// - Must be called from the main thread
179    /// - R cannot be safely shutdown and reinitialized
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if R initialization fails.
184    pub unsafe fn init(self) -> Result<REngine, REngineError> {
185        // Guard against re-initialization
186        if r_initialized_sentinel() {
187            return Err(REngineError::AlreadyInitialized);
188        }
189
190        ensure_r_home_env()?;
191
192        // Convert args to C strings
193        let c_args: Vec<CString> = self
194            .args
195            .iter()
196            .map(|s| CString::new(s.as_str()).unwrap())
197            .collect();
198
199        let mut c_ptrs: Vec<*mut c_char> = c_args.iter().map(|s| s.as_ptr().cast_mut()).collect();
200
201        let argc = c_ptrs.len() as c_int;
202        let argv = c_ptrs.as_mut_ptr();
203
204        // Initialize R.
205        //
206        // Note: `Rf_initEmbeddedR()` already calls `setup_Rmainloop()`.
207        // We want tighter control (and to avoid double-calling the setup),
208        // so we call `Rf_initialize_R()` directly and then `setup_Rmainloop()`.
209        let result = unsafe { Rf_initialize_R(argc, argv) };
210        if result != 0 {
211            return Err(REngineError::InitializationFailed);
212        }
213
214        unsafe {
215            // Set global flags *after* initialization, mirroring R's own
216            // `Rf_initEmbeddedR()` order (but respecting our builder flags).
217            set_r_interactive(if self.interactive { 1 } else { 0 });
218            set_r_signal_handlers(if self.signal_handlers { 1 } else { 0 });
219
220            // Disable R's C-stack overflow check before `setup_Rmainloop()`
221            // evaluates any R code. `Rf_initialize_R` calibrates
222            // `R_CStackStart` for the *process* main thread (glibc uses
223            // `__libc_stack_end`), so when R is initialized on any other
224            // thread — as the test harness's dedicated `r-test-main` thread
225            // does — the computed usage is garbage and R dies with
226            // "C stack usage <huge> is too close to the limit" during
227            // startup evaluation. macOS calibrates per-thread
228            // (`pthread_get_stackaddr_np`), which is why this only bites on
229            // Linux. Disabling the check (limit = -1, per Writing R
230            // Extensions §8) is standard embedded-R-on-a-thread practice;
231            // the OS guard page still catches real overflows.
232            R_CStackLimit = usize::MAX;
233
234            setup_Rmainloop();
235
236            // Note: We do NOT register an atexit handler for Rf_endEmbeddedR.
237            // The R runtime cleanup operations (KillAllDevices, RunExitFinalizers, etc.)
238            // are complex and can crash if other cleanup is happening concurrently.
239            // For short-lived programs (tests, benchmarks), letting the OS reclaim
240            // resources on process exit is safer and sufficient.
241        }
242
243        Ok(REngine)
244    }
245}
246
247/// Handle to an initialized R runtime.
248///
249/// This is a marker type indicating R has been initialized for this process.
250/// R cleanup (via `Rf_endEmbeddedR`) is intentionally NOT called because it
251/// performs non-reentrant operations that can crash if called during Drop
252/// or concurrent with other cleanup. The OS reclaims all resources on process exit.
253pub struct REngine;
254
255impl Drop for REngine {
256    /// Implements drop such that `std::mem::forget` leaks `REngine` rather than
257    /// dropping it, when `Drop` is absent.
258    fn drop(&mut self) {}
259}
260
261impl REngine {
262    /// Create a new builder for configuring R initialization.
263    pub fn build() -> REngineBuilder {
264        REngineBuilder::new()
265    }
266}
267
268// Note: We intentionally DO NOT provide shutdown or Drop implementations.
269//
270// Rf_endEmbeddedR performs non-reentrant cleanup operations.
271// Here's what it does (from R 4.5.2 source):
272//
273// Unix/Linux version (src/unix/Rembedded.c):
274// ```c
275// void Rf_endEmbeddedR(int fatal)
276// {
277//     R_RunExitFinalizers();    // Runs .Last and exit handlers (NOT reentrant!)
278//     CleanEd();                // Editor cleanup
279//     if(!fatal) KillAllDevices();  // Graphics devices (NOT reentrant!)
280//     R_CleanTempDir();         // File system cleanup
281//     if(!fatal && R_CollectWarnings)
282//         PrintWarnings();      // Console I/O
283//     fpu_setup(FALSE);         // FPU state
284// }
285// ```
286//
287// Windows version (src/gnuwin32/embeddedR.c):
288// ```c
289// void Rf_endEmbeddedR(int fatal)
290// {
291//     R_RunExitFinalizers();
292//     CleanEd();
293//     R_CleanTempDir();
294//     if(!fatal){
295//         Rf_KillAllDevices();
296//         AllDevicesKilled = TRUE;
297//     }
298//     if(!fatal && R_CollectWarnings)
299//         PrintWarnings();
300//     app_cleanup();           // Application-specific cleanup
301// }
302// ```
303//
304// These operations are NOT reentrant and must run exactly once at process exit.
305// Calling during Drop (e.g., test cleanup) causes crashes.
306//
307// **Solution:** We intentionally do NOT call Rf_endEmbeddedR. For short-lived
308// programs (tests, benchmarks), the OS reclaims all resources on process exit.
309// This avoids crashes from double-cleanup or reentrant calls.
310
311/// Errors that can occur during R engine initialization.
312#[derive(Debug)]
313pub enum REngineError {
314    /// Could not determine / set `R_HOME` for embedding.
315    RHomeNotFound {
316        /// Optional stderr from `R RHOME` command for diagnostics.
317        stderr: Option<String>,
318    },
319    /// R initialization failed.
320    InitializationFailed,
321    /// R is already initialized. Re-initialization is not supported.
322    AlreadyInitialized,
323}
324
325impl std::fmt::Display for REngineError {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        match self {
328            REngineError::RHomeNotFound { stderr } => {
329                write!(f, "R_HOME is not set and `R RHOME` could not be resolved")?;
330                if let Some(stderr) = stderr
331                    && !stderr.is_empty()
332                {
333                    write!(f, "\nstderr: {}", stderr)?;
334                }
335                Ok(())
336            }
337            REngineError::InitializationFailed => write!(f, "R initialization failed"),
338            REngineError::AlreadyInitialized => {
339                write!(
340                    f,
341                    "R is already initialized. Multiple calls to REngineBuilder::init() are not supported."
342                )
343            }
344        }
345    }
346}
347
348impl std::error::Error for REngineError {}
349
350fn ensure_r_home_env() -> Result<(), REngineError> {
351    // If R_HOME is already set, use it
352    if std::env::var_os("R_HOME").is_some() {
353        return Ok(());
354    }
355
356    // Auto-detect via `R RHOME`
357    let output = Command::new("R")
358        .args(["RHOME"])
359        .output()
360        .map_err(|_| REngineError::RHomeNotFound { stderr: None })?;
361
362    if !output.status.success() {
363        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
364        return Err(REngineError::RHomeNotFound {
365            stderr: Some(stderr),
366        });
367    }
368
369    let r_home = String::from_utf8(output.stdout)
370        .map_err(|_| REngineError::RHomeNotFound { stderr: None })?;
371    let r_home = r_home.trim();
372    if r_home.is_empty() {
373        return Err(REngineError::RHomeNotFound { stderr: None });
374    }
375
376    // SAFETY: We call this during single-threaded startup (before initializing
377    // R and before spawning any worker threads).
378    unsafe {
379        std::env::set_var("R_HOME", r_home);
380    }
381    Ok(())
382}
383
384#[cfg(test)]
385mod tests;