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 [`mod@crate::condition`])
17//!
18//! See [`crate::error_value`] for the tagged-SEXP layout, the
19//! `error_in_r` default + `no_error_in_r` / `unwrap_in_r` opt-outs, and the
20//! PROTECT-discipline gotcha that R-devel surfaces.
21//!
22//! ## When `Rf_error` still fires (framework-internal)
23//!
24//! `Rf_error` (longjmp via `r_stop`) survives only at FFI guard sites where
25//! there is no SEXP slot to return through:
26//!
27//! 1. **`ffi_guard::guarded_ffi_call(GuardMode::CatchUnwind, …)`** — worker
28//! thread panic conversion before the worker→main boundary returns a SEXP.
29//! 2. **`trait_abi::check_arity`** — pre-shim arity check that runs before the
30//! vtable shim has a SEXP to return.
31//!
32//! ALTREP `RUnwind` callbacks now route through
33//! `with_r_unwind_protect_sourced` → `raise_rust_condition_via_stop`, which
34//! preserves `rust_*` class layering without going through `r_stop`.
35//!
36//! `r_stop` is `pub(crate)` — no user code should depend on it.
37//!
38//! # Example
39//!
40//! ```ignore
41//! use miniextendr_api::miniextendr;
42//!
43//! #[miniextendr]
44//! fn validate_input(x: i32) -> i32 {
45//! assert!(x >= 0, "x must be non-negative, got {x}");
46//! x * 2
47//! }
48//! ```
49
50/// Raise an R error via `Rf_error` (longjmp). **Crate-internal only.**
51///
52/// Survives at two guard sites where there is no SEXP slot to return through:
53/// - [`crate::ffi_guard::guarded_ffi_call`] with `GuardMode::CatchUnwind`
54/// (worker thread panic conversion)
55/// - [`crate::trait_abi::check_arity`] (pre-shim arity check)
56///
57/// User code should use `panic!()` (caught by the framework and converted to a
58/// `rust_error` R condition) or the structured condition macros `error!()` /
59/// `warning!()` / `message!()` / `condition!()`.
60#[inline]
61pub(crate) fn r_stop(msg: &str) -> ! {
62 // Replace interior NUL bytes (user-derived messages can contain them); `CString::new`
63 // would otherwise reject them, and a panic here would abort the process.
64 let c_msg = std::ffi::CString::new(msg.replace('\0', "\u{fffd}")).unwrap_or_default();
65
66 if crate::worker::is_r_main_thread() {
67 unsafe {
68 crate::sys::Rf_error_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
69 }
70 } else {
71 // Route to main thread
72 crate::worker::with_r_thread(move || unsafe {
73 crate::sys::Rf_error_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
74 })
75 }
76}
77
78/// Raise an R warning with the given message.
79///
80/// Unlike `r_stop`, this returns normally after issuing the warning.
81/// Automatically routes to R's main thread if called from a worker thread.
82#[inline]
83pub fn r_warning(msg: &str) {
84 // NUL-safe (see r_stop)
85 let c_msg = std::ffi::CString::new(msg.replace('\0', "\u{fffd}")).unwrap_or_default();
86
87 if crate::worker::is_r_main_thread() {
88 unsafe {
89 crate::sys::Rf_warning_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
90 }
91 } else {
92 crate::worker::with_r_thread(move || unsafe {
93 crate::sys::Rf_warning_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
94 });
95 }
96}
97
98/// Print a message to R's console (internal implementation).
99/// Automatically routes to R's main thread if called from a worker thread.
100#[doc(hidden)]
101#[inline]
102pub fn _r_print_str(msg: &str) {
103 // NUL-safe (see r_stop)
104 let c_msg = std::ffi::CString::new(msg.replace('\0', "\u{fffd}")).unwrap_or_default();
105
106 if crate::worker::is_r_main_thread() {
107 unsafe {
108 crate::sys::Rprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
109 }
110 } else {
111 crate::worker::with_r_thread(move || unsafe {
112 crate::sys::Rprintf_unchecked(c"%s".as_ptr(), c_msg.as_ptr());
113 });
114 }
115}
116
117/// Print a newline to R's console (internal implementation).
118/// Automatically routes to R's main thread if called from a worker thread.
119#[doc(hidden)]
120#[inline]
121pub fn _r_print_newline() {
122 if crate::worker::is_r_main_thread() {
123 unsafe {
124 crate::sys::Rprintf_unchecked(c"\n".as_ptr());
125 }
126 } else {
127 crate::worker::with_r_thread(|| unsafe {
128 crate::sys::Rprintf_unchecked(c"\n".as_ptr());
129 });
130 }
131}
132
133/// Print to R's console (like `print!`).
134///
135/// # Example
136///
137/// ```ignore
138/// use miniextendr_api::r_print;
139///
140/// r_print!("Hello ");
141/// r_print!("value: {}", 42);
142/// ```
143#[macro_export]
144macro_rules! r_print {
145 () => {};
146 ($($arg:tt)*) => {
147 $crate::error::_r_print_str(&format!($($arg)*))
148 };
149}
150
151/// Print to R's console with a newline (like `println!`).
152///
153/// # Example
154///
155/// ```ignore
156/// use miniextendr_api::r_println;
157///
158/// r_println!(); // just a newline
159/// r_println!("Hello, world!");
160/// r_println!("value: {}", 42);
161/// ```
162#[macro_export]
163macro_rules! r_println {
164 () => {
165 $crate::error::_r_print_newline()
166 };
167 ($($arg:tt)*) => {{
168 $crate::error::_r_print_str(&format!($($arg)*));
169 $crate::error::_r_print_newline();
170 }};
171}