Skip to main content

miniextendr_api/
error.rs

1//! Error handling helpers for R API calls.
2//!
3//! ## User-facing path: tagged condition SEXP
4//!
5//! Every `#[miniextendr]` function runs inside
6//! [`with_r_unwind_protect`](crate::unwind_protect::with_r_unwind_protect).
7//! Rust panics and user-raised conditions (`error!()`, `warning!()`, `message!()`,
8//! `condition!()`) are caught, packaged as a tagged SEXP, and returned normally.
9//! The generated R wrapper inspects the SEXP and raises the appropriate R
10//! condition with `rust_*` class layering. **No `Rf_error` longjmp happens on
11//! this path.**
12//!
13//! User code should use:
14//! - `panic!()` — for unrecoverable Rust errors (becomes `rust_error` in R)
15//! - `error!()` / `warning!()` / `message!()` / `condition!()` — for structured
16//!   R conditions (see `crate::condition`)
17//!
18//! ## When `Rf_error` still fires (framework-internal)
19//!
20//! `Rf_error` (longjmp via `r_stop`) survives only at FFI guard sites where
21//! there is no SEXP slot to return through:
22//!
23//! 1. **`ffi_guard::guarded_ffi_call(GuardMode::CatchUnwind, …)`** — worker
24//!    thread panic conversion before the worker→main boundary returns a SEXP.
25//! 2. **`trait_abi::check_arity`** — pre-shim arity check that runs before the
26//!    vtable shim has a SEXP to return.
27//!
28//! ALTREP `RUnwind` callbacks now route through
29//! `with_r_unwind_protect_sourced` → `raise_rust_condition_via_stop`, which
30//! preserves `rust_*` class layering without going through `r_stop`.
31//!
32//! [`r_stop`] is `pub(crate)` — no user code should depend on it.
33//!
34//! # Example
35//!
36//! ```ignore
37//! use miniextendr_api::miniextendr;
38//!
39//! #[miniextendr]
40//! fn validate_input(x: i32) -> i32 {
41//!     assert!(x >= 0, "x must be non-negative, got {x}");
42//!     x * 2
43//! }
44//! ```
45
46/// Raise an R error via `Rf_error` (longjmp). **Crate-internal only.**
47///
48/// Survives at two guard sites where there is no SEXP slot to return through:
49/// - [`crate::ffi_guard::guarded_ffi_call`] with `GuardMode::CatchUnwind`
50///   (worker thread panic conversion)
51/// - [`crate::trait_abi::check_arity`] (pre-shim arity check)
52///
53/// User code should use `panic!()` (caught by the framework and converted to a
54/// `rust_error` R condition) or the structured condition macros `error!()` /
55/// `warning!()` / `message!()` / `condition!()`.
56///
57/// # Panics
58///
59/// Panics if the message contains null bytes.
60#[inline]
61pub(crate) fn r_stop(msg: &str) -> ! {
62    let c_msg = std::ffi::CString::new(msg).expect("r_stop: message contains null bytes");
63
64    if crate::worker::is_r_main_thread() {
65        unsafe {
66            crate::ffi::Rf_error_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
67        }
68    } else {
69        // Route to main thread
70        crate::worker::with_r_thread(move || unsafe {
71            crate::ffi::Rf_error_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
72        })
73    }
74}
75
76/// Raise an R warning with the given message.
77///
78/// Unlike `r_stop`, this returns normally after issuing the warning.
79/// Automatically routes to R's main thread if called from a worker thread.
80#[inline]
81pub fn r_warning(msg: &str) {
82    let c_msg = std::ffi::CString::new(msg).expect("r_warning: message contains null bytes");
83
84    if crate::worker::is_r_main_thread() {
85        unsafe {
86            crate::ffi::Rf_warning_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
87        }
88    } else {
89        crate::worker::with_r_thread(move || unsafe {
90            crate::ffi::Rf_warning_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
91        });
92    }
93}
94
95/// Print a message to R's console (internal implementation).
96/// Automatically routes to R's main thread if called from a worker thread.
97#[doc(hidden)]
98#[inline]
99pub fn _r_print_str(msg: &str) {
100    let c_msg = std::ffi::CString::new(msg).expect("r_print!: message contains null bytes");
101
102    if crate::worker::is_r_main_thread() {
103        unsafe {
104            crate::ffi::Rprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
105        }
106    } else {
107        crate::worker::with_r_thread(move || unsafe {
108            crate::ffi::Rprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
109        });
110    }
111}
112
113/// Print a newline to R's console (internal implementation).
114/// Automatically routes to R's main thread if called from a worker thread.
115#[doc(hidden)]
116#[inline]
117pub fn _r_print_newline() {
118    if crate::worker::is_r_main_thread() {
119        unsafe {
120            crate::ffi::Rprintf_unchecked(c"\n".as_ptr());
121        }
122    } else {
123        crate::worker::with_r_thread(|| unsafe {
124            crate::ffi::Rprintf_unchecked(c"\n".as_ptr());
125        });
126    }
127}
128
129/// Print to R's console (like `print!`).
130///
131/// # Example
132///
133/// ```ignore
134/// use miniextendr_api::r_print;
135///
136/// r_print!("Hello ");
137/// r_print!("value: {}", 42);
138/// ```
139#[macro_export]
140macro_rules! r_print {
141    () => {};
142    ($($arg:tt)*) => {
143        $crate::error::_r_print_str(&format!($($arg)*))
144    };
145}
146
147/// Print to R's console with a newline (like `println!`).
148///
149/// # Example
150///
151/// ```ignore
152/// use miniextendr_api::r_println;
153///
154/// r_println!();  // just a newline
155/// r_println!("Hello, world!");
156/// r_println!("value: {}", 42);
157/// ```
158#[macro_export]
159macro_rules! r_println {
160    () => {
161        $crate::error::_r_print_newline()
162    };
163    ($($arg:tt)*) => {{
164        $crate::error::_r_print_str(&format!($($arg)*));
165        $crate::error::_r_print_newline();
166    }};
167}