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;