miniextendr_api/error_value.rs
1//! Tagged condition value transport.
2//!
3//! Rust-origin failures (panics, `Result::Err`, `Option::None`) and user-raised
4//! conditions (`error!()`, `warning!()`, `message!()`, `condition!()`) are
5//! converted to a tagged SEXP value instead of raising an R error immediately.
6//! The generated R wrapper inspects this tagged value and escalates it to a
7//! proper R condition past the Rust boundary, with `rust_*` class layering.
8//!
9//! # Why tagged SEXP instead of `Rf_error`
10//!
11//! The naive way to surface a Rust error in R is to call `Rf_error`, which
12//! `longjmp`s out of the call frame. That works for C — C has no destructors —
13//! but in Rust it skips every drop on the stack: open files, `Mutex` guards,
14//! `Box::into_raw` round-trips, the worker-thread continuation token. Anything
15//! holding a resource leaks or corrupts.
16//!
17//! The framework instead catches every Rust panic (and every `RCondition`
18//! `panic_any` payload) at the boundary inside
19//! [`crate::unwind_protect::with_r_unwind_protect`], encodes it as the
20//! 4-element list described below, and *returns* that SEXP normally. The
21//! generated R wrapper then re-raises with `stop(structure(..., class =
22//! c("rust_*", ...)))`. Destructors run; `tryCatch` sees the right class.
23//!
24//! There is one accepted leak: on the R-longjmp branch inside
25//! `with_r_unwind_protect` (when an R-origin error is propagated through via
26//! `R_ContinueUnwind`), the `RErrorMarker` panic payload — about 8 bytes plus
27//! `Box` header — escapes Rust drop ordering. This is the price we pay for
28//! routing Rust failures through real R conditions instead of
29//! `Rf_error`-via-longjmp, and is exactly why lint MXL300 forbids direct
30//! `Rf_error` / `Rf_errorcall` in user code: every `Rf_error` skips Rust
31//! destructors unconditionally, not just on the (rare) R-longjmp path.
32//!
33//! # The three error-emission entry points
34//!
35//! Authors of `#[miniextendr]` functions reach for one of:
36//!
37//! 1. **`panic!(msg)`** — escape hatch. Produces `kind = "panic"` and R class
38//! `c("rust_error", "simpleError", "error", "condition")`. Use for true
39//! bugs / impossible states; the caller has nothing to catch by class.
40//! 2. **`miniextendr_api::error!("msg")`** — typed condition. Produces `kind
41//! = "error"` and the same `rust_error` class layering. The `class =
42//! "my_class"` form prepends a user class, giving R-side
43//! `c("rust_my_class", "rust_error", "simpleError", "error", "condition")`
44//! — exactly what a caller's `tryCatch(my_class = …)` matches on. The
45//! sibling [`crate::warning!`], [`crate::message!`], [`crate::condition!`]
46//! macros cover the non-error condition kinds.
47//! 3. **`Result<_, E>` where `E: std::error::Error`**, often via
48//! [`crate::condition::AsRError`] — value-style propagation through Rust
49//! code. Converts at the boundary using `kind = "result_err"`.
50//! `Option::None` follows the same path with `kind = "none_err"`.
51//!
52//! # `error_in_r` is the default
53//!
54//! For every `#[miniextendr]` fn / method, the proc macro emits a wrapper that
55//! routes through this tagged-SEXP transport — i.e. `error_in_r = true` is the
56//! default. The opt-outs are documented on the macro:
57//!
58//! - `#[miniextendr(no_error_in_r)]` — bypass the tagged-SEXP path entirely.
59//! Useful for trait-ABI vtable shims and benchmarks; Rust panics become
60//! classic `Rf_error` longjmps. Drops the leak above at the cost of skipping
61//! Rust destructors universally.
62//! - `#[miniextendr(unwrap_in_r)]` — `Result<T, E>` returns are unwrapped on
63//! the R side rather than encoded as `kind = "result_err"`. Orthogonal to
64//! the transport: still rides this SEXP path, just changes how `Err` is
65//! stringified.
66//!
67//! Older comments suggesting `Rf_error` is the user-facing path predate PR
68//! #344 and are wrong. The wrapper preambles now consistently use this
69//! transport.
70//!
71//! # Condition value structure (`make_rust_condition_value`)
72//!
73//! The tagged SEXP is a 5-element named list:
74//! - `error`: error message (character scalar)
75//! - `kind`: condition kind string — one of the constants in [`kind`]
76//! - `class`: optional user-supplied custom class (character scalar or `NULL`)
77//! - `call`: the R call SEXP (or `NULL` if not available)
78//! - `data`: optional named-list condition-data payload (from the macros'
79//! `data = ...` form), or `NULL`. The R helper splices these named fields
80//! into the condition object so handlers can read `e$<name>`.
81//! - class attribute: `"rust_condition_value"`
82//! - `__rust_condition__` attribute: `TRUE`
83//!
84//! # PROTECT discipline (read before editing)
85//!
86//! [`make_rust_condition_value`] allocates SEXPs that must remain live
87//! across subsequent allocations (`SET_VECTOR_ELT` / `SETATTRIB` both
88//! trigger old-to-new GC barriers): the list itself, the message scalar
89//! STRSXP, the kind scalar STRSXP, the optional class scalar STRSXP, the
90//! `TRUE` marker LGLSXP, and — when a `data` payload is present — the data
91//! VECSXP, its names STRSXP, and each materialised field value. Each is
92//! `Rf_protect`ed before the next allocation; `prot` counts them;
93//! `Rf_unprotect(prot)` balances at exit on every branch. Field values are
94//! materialised one at a time and rooted into the protected data list
95//! immediately (same shape as `List::from_pairs`) so an unrooted value SEXP
96//! never survives across the next allocation.
97//!
98//! R-devel runs a more aggressive GC than R-release/oldrel and *will* fire
99//! inside the window between two allocations. PR #344 commit `af6b4875`
100//! tracked down a `recursive gc invocation` segfault that lit up only on
101//! R-devel because the pre-existing 3-element version was lucky-not-safe;
102//! adding the class slot crossed the threshold. **If you add another fresh
103//! allocation, protect it.** A green R-release CI run is *not* proof of
104//! safety here; run `gctorture(TRUE)` on R-devel before merging.
105
106use crate::cached_class::{
107 condition_names_sexp, rust_condition_attr_symbol, rust_condition_class_sexp,
108};
109use crate::sexp_types::CE_UTF8;
110use crate::sys::{self};
111use crate::{IntoR, SEXP, SEXPTYPE, SexpExt};
112
113/// Canonical kind strings for tagged condition values.
114///
115/// These constants are emitted into the `kind` slot of
116/// [`make_rust_condition_value`] and consumed by the R-side
117/// `.miniextendr_raise_condition` switch (see
118/// `registry::write_r_wrappers_to_file`). Reference these constants
119/// from codegen and runtime sites instead of bare string literals so a
120/// typo cannot silently change which switch arm fires.
121///
122/// The constants are kept in lockstep with the generated R helper; if a new
123/// kind is added, both the emission site and the R helper need to learn it.
124pub mod kind {
125 /// Default kind for Rust panics that surface to R via the generic panic
126 /// path (no `RCondition` payload). Layered as `rust_error`.
127 pub const PANIC: &str = "panic";
128 /// `Result<_, E>::Err(...)` formatted via `Debug` (raised when the user
129 /// returns an `Err` from a `#[miniextendr]` fn/method).
130 pub const RESULT_ERR: &str = "result_err";
131 /// `Option<T>::None` reached where a value was required (raised by the
132 /// `NoneOnErr` / required-Option return paths).
133 pub const NONE_ERR: &str = "none_err";
134 /// `TryFromSexp` / coerce / strict-mode conversion failed at argument
135 /// unmarshalling.
136 pub const CONVERSION: &str = "conversion";
137 /// User-raised `error!(...)` condition.
138 pub const ERROR: &str = "error";
139 /// User-raised `warning!(...)` condition.
140 pub const WARNING: &str = "warning";
141 /// User-raised `message!(...)` condition.
142 pub const MESSAGE: &str = "message";
143 /// User-raised `condition!(...)` condition.
144 pub const CONDITION: &str = "condition";
145 /// Fallback kind written by [`super::make_rust_condition_value`] when the
146 /// caller's `kind` argument contained an interior NUL and could not be
147 /// converted to a `CString`. Should not appear in normal flow; the match
148 /// arm in [`crate::condition::RCondition::from_tagged_sexp`] handles it
149 /// defensively by degrading to `RCondition::Error`.
150 pub const OTHER_RUST_ERROR: &str = "other_rust_error";
151}
152
153/// Convert a `&str` to a `CString`, falling back to `fallback` on interior NUL bytes.
154///
155/// Used internally by [`make_rust_condition_value`] to avoid duplicating the
156/// `CString::new(s).unwrap_or_else(…)` pattern across every slot.
157fn to_cstring_lossy(s: &str, fallback: &str) -> std::ffi::CString {
158 std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new(fallback).unwrap())
159}
160
161/// Build a tagged condition value with no structured `data` payload.
162///
163/// Thin wrapper over [`make_rust_condition_value_with_data`] with `data =
164/// None`. This is the entry point used by all proc-macro-generated codegen
165/// (argument-conversion failures, `Option::None`, `Result::Err`), none of
166/// which carries a `data` payload. Only the user-facing `error!()` /
167/// `warning!()` / `message!()` / `condition!()` macros (routed through
168/// [`crate::unwind_protect`]) attach `data`.
169///
170/// # Safety
171///
172/// See [`make_rust_condition_value_with_data`].
173#[inline]
174pub fn make_rust_condition_value(
175 message: &str,
176 kind: &str,
177 class: Option<&str>,
178 call: Option<SEXP>,
179) -> SEXP {
180 make_rust_condition_value_with_data(message, kind, class, call, None)
181}
182
183/// Build a tagged condition-value SEXP for transport across the Rust→R boundary.
184///
185/// Used for all Rust-origin failures and user-facing conditions. The R-side
186/// switch in `condition_check_lines` reads `.val$kind` to select the condition
187/// type and `.val$class` to prepend optional user classes before the standard
188/// `rust_*` layering.
189///
190/// # Safety
191///
192/// Must be called from R's main thread (standard R API constraint).
193/// The returned SEXP is unprotected — caller must protect if needed.
194///
195/// # PROTECT discipline
196///
197/// Every fresh allocation (msg, kind, optional class, true-marker, and — when
198/// present — the `data` VECSXP, its names, and each field value) is protected
199/// before the next allocation that might trigger a GC barrier. The `prot`
200/// counter is incremented on each `Rf_protect` and balanced by
201/// `Rf_unprotect(prot)` at exit on all branches. This pattern was established
202/// by PR #344 commit `af6b4875` to fix a `recursive gc invocation` segfault on
203/// R-devel.
204///
205/// # Arguments
206///
207/// * `message` - Human-readable condition message
208/// * `kind` - Condition kind — one of the constants in [`kind`].
209/// * `class` - Optional user-supplied class name to prepend to the layered vector
210/// * `call` - Optional R call SEXP for error context. When `None`, uses `R_NilValue`.
211/// * `data` - Optional named condition-data payload (from the macros' `data =
212/// ...` form). When `Some`, each `(name, value)` becomes a named element of a
213/// list stored in slot `[4]`; the R helper splices these into the condition
214/// object so handlers can read `e$<name>`. When `None`, slot `[4]` is `NULL`.
215pub fn make_rust_condition_value_with_data(
216 message: &str,
217 kind: &str,
218 class: Option<&str>,
219 call: Option<SEXP>,
220 data: Option<crate::condition::ConditionData>,
221) -> SEXP {
222 unsafe {
223 // PROTECT discipline: every fresh allocation that's live across another
224 // allocation must be protected. SET_VECTOR_ELT and SETATTRIB can both
225 // trigger old-to-new GC barriers; R-devel's GC fires more aggressively
226 // here than R-release/oldrel, so unprotected intermediates corrupt the
227 // heap on R-devel even when R 4.5/4.4 happen to survive (PR #344 fix).
228 let list = sys::Rf_allocVector(SEXPTYPE::VECSXP, 5);
229 sys::Rf_protect(list);
230 let mut prot = 1;
231
232 // Element 0: error message
233 let msg_cstr = to_cstring_lossy(message, "<invalid error message>");
234 let msg_charsxp = sys::Rf_mkCharCE(msg_cstr.as_ptr(), CE_UTF8);
235 let msg_sexp = SEXP::scalar_string(msg_charsxp);
236 sys::Rf_protect(msg_sexp);
237 prot += 1;
238 list.set_vector_elt(0, msg_sexp);
239
240 // Element 1: kind string
241 let kind_cstr = to_cstring_lossy(kind, kind::OTHER_RUST_ERROR);
242 let kind_charsxp = sys::Rf_mkCharCE(kind_cstr.as_ptr(), CE_UTF8);
243 let kind_sexp = SEXP::scalar_string(kind_charsxp);
244 sys::Rf_protect(kind_sexp);
245 prot += 1;
246 list.set_vector_elt(1, kind_sexp);
247
248 // Element 2: optional custom class (NULL when not provided).
249 // Only the Some-branch allocates; nil is constant.
250 let class_sexp = if let Some(class_name) = class {
251 let class_cstr = to_cstring_lossy(class_name, "rust_condition");
252 let class_charsxp = sys::Rf_mkCharCE(class_cstr.as_ptr(), CE_UTF8);
253 let s = SEXP::scalar_string(class_charsxp);
254 sys::Rf_protect(s);
255 prot += 1;
256 s
257 } else {
258 SEXP::nil()
259 };
260 list.set_vector_elt(2, class_sexp);
261
262 // Element 3: caller-owned SEXP — already protected (or R_NilValue)
263 list.set_vector_elt(3, call.unwrap_or(SEXP::nil()));
264
265 // Element 4: optional named-list condition data (NULL when absent).
266 //
267 // PROTECT discipline: we build a fresh VECSXP `data_list` plus a STRSXP
268 // `data_names`, and each field's materialised SEXP. Every one of these
269 // is live across subsequent allocations, so each is protected before
270 // the next alloc:
271 // - data_list protected before any field materialisation,
272 // - data_names protected before per-field CHARSXP allocations,
273 // - each field value materialised then immediately stored into the
274 // protected data_list (so it is rooted by the list before the next
275 // field allocates — same shape as `List::from_pairs`).
276 let data_sexp = if let Some(fields) = data {
277 let n: isize = fields
278 .len()
279 .try_into()
280 .expect("condition data length exceeds isize::MAX");
281 let data_list = sys::Rf_allocVector(SEXPTYPE::VECSXP, n);
282 sys::Rf_protect(data_list);
283 prot += 1;
284 let data_names = sys::Rf_allocVector(SEXPTYPE::STRSXP, n);
285 sys::Rf_protect(data_names);
286 prot += 1;
287 for (i, (name, value)) in fields.into_iter().enumerate() {
288 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
289 // Materialise the value and immediately root it in data_list
290 // (protected) before the name CHARSXP allocation below.
291 let value_sexp = value.into_sexp();
292 data_list.set_vector_elt(idx, value_sexp);
293 let name_cstr = to_cstring_lossy(&name, "<invalid name>");
294 let name_charsxp = sys::Rf_mkCharCE(name_cstr.as_ptr(), CE_UTF8);
295 data_names.set_string_elt(idx, name_charsxp);
296 }
297 data_list.set_names(data_names);
298 data_list
299 } else {
300 SEXP::nil()
301 };
302 list.set_vector_elt(4, data_sexp);
303
304 // Names / class symbols are cached. The TRUE marker on set_attr is a
305 // fresh LGLSXP — protect across the SETATTRIB call.
306 list.set_names(condition_names_sexp());
307 list.set_class(rust_condition_class_sexp());
308 let true_marker = SEXP::scalar_logical(true);
309 sys::Rf_protect(true_marker);
310 prot += 1;
311 list.set_attr(rust_condition_attr_symbol(), true_marker);
312
313 sys::Rf_unprotect(prot);
314 list
315 }
316}