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//! added to a single [`ProtectScope`](crate::ProtectScope) before the next
93//! allocation; the scope releases them all together (`UNPROTECT`) when it
94//! drops at function exit — on every branch. Field values are
95//! materialised one at a time and rooted into the protected data list
96//! immediately (same shape as `List::from_pairs`) so an unrooted value SEXP
97//! never survives across the next allocation.
98//!
99//! R-devel runs a more aggressive GC than R-release/oldrel and *will* fire
100//! inside the window between two allocations. PR #344 commit `af6b4875`
101//! tracked down a `recursive gc invocation` segfault that lit up only on
102//! R-devel because the pre-existing 3-element version was lucky-not-safe;
103//! adding the class slot crossed the threshold. **If you add another fresh
104//! allocation, protect it.** A green R-release CI run is *not* proof of
105//! safety here; run `gctorture(TRUE)` on R-devel before merging.
106
107use crate::cached_class::{
108 condition_names_sexp, rust_condition_attr_symbol, rust_condition_class_sexp,
109};
110use crate::sexp_types::CE_UTF8;
111use crate::sys::{self};
112use crate::{IntoR, SEXP, SEXPTYPE, SexpExt};
113
114/// Canonical kind strings for tagged condition values.
115///
116/// These constants are emitted into the `kind` slot of
117/// [`make_rust_condition_value`] and consumed by the R-side
118/// `.miniextendr_raise_condition` switch (see
119/// `registry::write_r_wrappers_to_file`). Reference these constants
120/// from codegen and runtime sites instead of bare string literals so a
121/// typo cannot silently change which switch arm fires.
122///
123/// The constants are kept in lockstep with the generated R helper; if a new
124/// kind is added, both the emission site and the R helper need to learn it.
125pub mod kind {
126 /// Default kind for Rust panics that surface to R via the generic panic
127 /// path (no `RCondition` payload). Layered as `rust_error`.
128 pub const PANIC: &str = "panic";
129 /// `Result<_, E>::Err(...)` formatted via `Debug` (raised when the user
130 /// returns an `Err` from a `#[miniextendr]` fn/method).
131 pub const RESULT_ERR: &str = "result_err";
132 /// `Option<T>::None` reached where a value was required (raised by the
133 /// `NoneOnErr` / required-Option return paths).
134 pub const NONE_ERR: &str = "none_err";
135 /// `TryFromSexp` / coerce / strict-mode conversion failed at argument
136 /// unmarshalling.
137 pub const CONVERSION: &str = "conversion";
138 /// User-raised `error!(...)` condition.
139 pub const ERROR: &str = "error";
140 /// User-raised `warning!(...)` condition.
141 pub const WARNING: &str = "warning";
142 /// User-raised `message!(...)` condition.
143 pub const MESSAGE: &str = "message";
144 /// User-raised `condition!(...)` condition.
145 pub const CONDITION: &str = "condition";
146 /// Fallback kind written by [`super::make_rust_condition_value`] when the
147 /// caller's `kind` argument contained an interior NUL and could not be
148 /// converted to a `CString`. Should not appear in normal flow; the match
149 /// arm in [`crate::condition::RCondition::from_tagged_sexp`] handles it
150 /// defensively by degrading to `RCondition::Error`.
151 pub const OTHER_RUST_ERROR: &str = "other_rust_error";
152}
153
154/// Convert a `&str` to a `CString`, falling back to `fallback` on interior NUL bytes.
155///
156/// Used internally by [`make_rust_condition_value`] to avoid duplicating the
157/// `CString::new(s).unwrap_or_else(…)` pattern across every slot.
158fn to_cstring_lossy(s: &str, fallback: &str) -> std::ffi::CString {
159 std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new(fallback).unwrap())
160}
161
162/// Build a tagged condition value with no structured `data` payload.
163///
164/// Thin wrapper over [`make_rust_condition_value_with_data`] with `data =
165/// None`. This is the entry point used by all proc-macro-generated codegen
166/// (argument-conversion failures, `Option::None`, `Result::Err`), none of
167/// which carries a `data` payload. Only the user-facing `error!()` /
168/// `warning!()` / `message!()` / `condition!()` macros (routed through
169/// [`crate::unwind_protect`]) attach `data`.
170///
171/// # Safety
172///
173/// Must be called from R's main thread in a valid R allocation context — the
174/// body performs raw R allocation (`Rf_allocVector`, `SET_VECTOR_ELT`,
175/// `SETATTRIB`) which is UB off the main thread. See
176/// [`make_rust_condition_value_with_data`] for the full contract.
177#[inline]
178pub unsafe fn make_rust_condition_value(
179 message: &str,
180 kind: &str,
181 class: Option<&str>,
182 call: Option<SEXP>,
183) -> SEXP {
184 // SAFETY: caller upholds the main-thread + valid-allocation-context contract.
185 unsafe { make_rust_condition_value_with_data(message, kind, class, call, None) }
186}
187
188/// Build a tagged condition-value SEXP for transport across the Rust→R boundary.
189///
190/// Used for all Rust-origin failures and user-facing conditions. The R-side
191/// switch in `condition_check_lines` reads `.val$kind` to select the condition
192/// type and `.val$class` to prepend optional user classes before the standard
193/// `rust_*` layering.
194///
195/// # Safety
196///
197/// Must be called from R's main thread in a valid R allocation context
198/// (standard R API constraint): the body performs raw R allocation
199/// (`Rf_allocVector`, `SET_VECTOR_ELT`, `SETATTRIB`) which is UB off the main
200/// thread. In practice every caller reaches this through
201/// [`crate::unwind_protect::with_r_unwind_protect`] (or a proc-macro-generated
202/// panic handler), both of which run on the main thread.
203/// The returned SEXP is unprotected — caller must protect if needed.
204///
205/// # PROTECT discipline
206///
207/// Every fresh allocation (msg, kind, optional class, true-marker, and — when
208/// present — the `data` VECSXP, its names, and each field value) is added to a
209/// single [`ProtectScope`](crate::ProtectScope) before the next allocation
210/// that might trigger a GC barrier. The scope releases them all together
211/// (`UNPROTECT`) when it drops at function exit, on every branch — the RAII
212/// equivalent of the former hand-counted `prot` / `Rf_unprotect(prot)` ladder.
213/// This discipline was established by PR #344 commit `af6b4875` to fix a
214/// `recursive gc invocation` segfault on R-devel.
215///
216/// # Arguments
217///
218/// * `message` - Human-readable condition message
219/// * `kind` - Condition kind — one of the constants in [`kind`].
220/// * `class` - Optional user-supplied class name to prepend to the layered vector
221/// * `call` - Optional R call SEXP for error context. When `None`, uses `R_NilValue`.
222/// * `data` - Optional named condition-data payload (from the macros' `data =
223/// ...` form). When `Some`, each `(name, value)` becomes a named element of a
224/// list stored in slot `[4]`; the R helper splices these into the condition
225/// object so handlers can read `e$<name>`. When `None`, slot `[4]` is `NULL`.
226pub unsafe fn make_rust_condition_value_with_data(
227 message: &str,
228 kind: &str,
229 class: Option<&str>,
230 call: Option<SEXP>,
231 data: Option<crate::condition::ConditionData>,
232) -> SEXP {
233 unsafe {
234 // PROTECT discipline: every fresh allocation that's live across another
235 // allocation must be protected. SET_VECTOR_ELT and SETATTRIB can both
236 // trigger old-to-new GC barriers; R-devel's GC fires more aggressively
237 // here than R-release/oldrel, so unprotected intermediates corrupt the
238 // heap on R-devel even when R 4.5/4.4 happen to survive (PR #344 fix).
239 // A single ProtectScope roots every intermediate; it releases them all
240 // when it drops at function exit.
241 let scope = crate::ProtectScope::new();
242 let list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, 5));
243
244 // Element 0: error message
245 let msg_cstr = to_cstring_lossy(message, "<invalid error message>");
246 let msg_charsxp = sys::Rf_mkCharCE(msg_cstr.as_ptr(), CE_UTF8);
247 let msg_sexp = scope.protect_raw(SEXP::scalar_string(msg_charsxp));
248 list.set_vector_elt(0, msg_sexp);
249
250 // Element 1: kind string
251 let kind_cstr = to_cstring_lossy(kind, kind::OTHER_RUST_ERROR);
252 let kind_charsxp = sys::Rf_mkCharCE(kind_cstr.as_ptr(), CE_UTF8);
253 let kind_sexp = scope.protect_raw(SEXP::scalar_string(kind_charsxp));
254 list.set_vector_elt(1, kind_sexp);
255
256 // Element 2: optional custom class (NULL when not provided).
257 // Only the Some-branch allocates; nil is constant.
258 let class_sexp = if let Some(class_name) = class {
259 let class_cstr = to_cstring_lossy(class_name, "rust_condition");
260 let class_charsxp = sys::Rf_mkCharCE(class_cstr.as_ptr(), CE_UTF8);
261 scope.protect_raw(SEXP::scalar_string(class_charsxp))
262 } else {
263 SEXP::nil()
264 };
265 list.set_vector_elt(2, class_sexp);
266
267 // Element 3: caller-owned SEXP — already protected (or R_NilValue)
268 list.set_vector_elt(3, call.unwrap_or(SEXP::nil()));
269
270 // Element 4: optional named-list condition data (NULL when absent).
271 //
272 // PROTECT discipline: we build a fresh VECSXP `data_list` plus a STRSXP
273 // `data_names`, and each field's materialised SEXP. Every one of these
274 // is live across subsequent allocations, so each is protected before
275 // the next alloc:
276 // - data_list protected before any field materialisation,
277 // - data_names protected before per-field CHARSXP allocations,
278 // - each field value materialised then immediately stored into the
279 // protected data_list (so it is rooted by the list before the next
280 // field allocates — same shape as `List::from_pairs`).
281 let data_sexp = if let Some(fields) = data {
282 let n: isize = fields
283 .len()
284 .try_into()
285 .expect("condition data length exceeds isize::MAX");
286 let data_list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, n));
287 let data_names = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::STRSXP, n));
288 for (i, (name, value)) in fields.into_iter().enumerate() {
289 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
290 // Materialise the value and immediately root it in data_list
291 // (protected) before the name CHARSXP allocation below.
292 let value_sexp = value.into_sexp();
293 data_list.set_vector_elt(idx, value_sexp);
294 let name_cstr = to_cstring_lossy(&name, "<invalid name>");
295 let name_charsxp = sys::Rf_mkCharCE(name_cstr.as_ptr(), CE_UTF8);
296 data_names.set_string_elt(idx, name_charsxp);
297 }
298 data_list.set_names(data_names);
299 data_list
300 } else {
301 SEXP::nil()
302 };
303 list.set_vector_elt(4, data_sexp);
304
305 // Names / class symbols are cached. The TRUE marker on set_attr is a
306 // fresh LGLSXP — protect across the SETATTRIB call.
307 list.set_names(condition_names_sexp());
308 list.set_class(rust_condition_class_sexp());
309 let true_marker = scope.protect_raw(SEXP::scalar_logical(true));
310 list.set_attr(rust_condition_attr_symbol(), true_marker);
311
312 list
313 }
314}