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}