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, `NULL` if not available (the R helper then uses
78//! the wrapper's call), or `FALSE` for a condition raised without a call
79//! ([`ConditionCall::None`]: the helper signals it with `call = NULL`, like
80//! R's `call. = FALSE`)
81//! - `data`: optional named-list condition-data payload (from the macros'
82//! `data = ...` form), or `NULL`. The R helper splices these named fields
83//! into the condition object so handlers can read `e$<name>`.
84//! - class attribute: `"rust_condition_value"`
85//! - `__rust_condition__` attribute: `TRUE`
86//!
87//! # PROTECT discipline (read before editing)
88//!
89//! [`make_rust_condition_value`] allocates SEXPs that must remain live
90//! across subsequent allocations (`SET_VECTOR_ELT` / `SETATTRIB` both
91//! trigger old-to-new GC barriers): the list itself, the message scalar
92//! STRSXP, the kind scalar STRSXP, the optional class scalar STRSXP, the
93//! `FALSE` no-call marker LGLSXP when the condition has no call, the
94//! `TRUE` marker LGLSXP, and — when a `data` payload is present — the data
95//! VECSXP, its names STRSXP, and each materialised field value. Each is
96//! added to a single [`ProtectScope`](crate::ProtectScope) before the next
97//! allocation; the scope releases them all together (`UNPROTECT`) when it
98//! drops at function exit — on every branch. Field values are
99//! materialised one at a time and rooted into the protected data list
100//! immediately (same shape as `List::from_pairs`) so an unrooted value SEXP
101//! never survives across the next allocation.
102//!
103//! R-devel runs a more aggressive GC than R-release/oldrel and *will* fire
104//! inside the window between two allocations. PR #344 commit `af6b4875`
105//! tracked down a `recursive gc invocation` segfault that lit up only on
106//! R-devel because the pre-existing 3-element version was lucky-not-safe;
107//! adding the class slot crossed the threshold. **If you add another fresh
108//! allocation, protect it.** A green R-release CI run is *not* proof of
109//! safety here; run `gctorture(TRUE)` on R-devel before merging.
110
111use crate::cached_class::{
112 condition_names_sexp, rust_condition_attr_symbol, rust_condition_class_sexp,
113};
114use crate::condition::{ConditionCall, ErrParts, RCondition};
115use crate::sexp_types::CE_UTF8;
116use crate::sys::{self};
117use crate::{IntoR, SEXP, SEXPTYPE, SexpExt};
118
119/// Canonical kind strings for tagged condition values.
120///
121/// These constants are emitted into the `kind` slot of
122/// [`make_rust_condition_value`] and consumed by the R-side
123/// `.miniextendr_raise_condition` switch (see
124/// `registry::write_r_wrappers_to_file`). Reference these constants
125/// from codegen and runtime sites instead of bare string literals so a
126/// typo cannot silently change which switch arm fires.
127///
128/// The constants are kept in lockstep with the generated R helper; if a new
129/// kind is added, both the emission site and the R helper need to learn it.
130pub mod kind {
131 /// Default kind for Rust panics that surface to R via the generic panic
132 /// path (no `RCondition` payload). Layered as `rust_error`.
133 pub const PANIC: &str = "panic";
134 /// `Result<_, E>::Err(...)` formatted via `Debug` (raised when the user
135 /// returns an `Err` from a `#[miniextendr]` fn/method).
136 pub const RESULT_ERR: &str = "result_err";
137 /// `Option<T>::None` reached where a value was required (raised by the
138 /// `NoneOnErr` / required-Option return paths).
139 pub const NONE_ERR: &str = "none_err";
140 /// An argument error: a `TryFromSexp` / coerce / strict-mode conversion
141 /// failed at argument unmarshalling, a post-conversion check refused the
142 /// value, or a body raised `arg_error!(...)`.
143 pub const CONVERSION: &str = "conversion";
144 /// User-raised `error!(...)` condition.
145 pub const ERROR: &str = "error";
146 /// User-raised `warning!(...)` condition.
147 pub const WARNING: &str = "warning";
148 /// User-raised `message!(...)` condition.
149 pub const MESSAGE: &str = "message";
150 /// User-raised `condition!(...)` condition.
151 pub const CONDITION: &str = "condition";
152 /// Fallback kind written by [`super::make_rust_condition_value`] when the
153 /// caller's `kind` argument contained an interior NUL and could not be
154 /// converted to a `CString`. Should not appear in normal flow; the match
155 /// arm in [`crate::condition::RCondition::from_tagged_sexp`] handles it
156 /// defensively by degrading to `RCondition::Error`.
157 pub const OTHER_RUST_ERROR: &str = "other_rust_error";
158}
159
160/// Convert a `&str` to a `CString`, falling back to `fallback` on interior NUL bytes.
161///
162/// Used internally by [`make_rust_condition_value`] to avoid duplicating the
163/// `CString::new(s).unwrap_or_else(…)` pattern across every slot.
164fn to_cstring_lossy(s: &str, fallback: &str) -> std::ffi::CString {
165 std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new(fallback).unwrap())
166}
167
168/// Build a tagged condition value with no structured `data` payload.
169///
170/// Thin wrapper over [`make_rust_condition_value_with_data`] with `data =
171/// None` and at most one class. Generated code uses it for the unclassed
172/// kinds (`Option::None`); classed `Result::Err` values go through
173/// [`result_err_condition_value`], argument-conversion failures through
174/// [`conversion_condition_value`], a post-conversion `no_na` refusal through
175/// [`arg_check_condition_value`], and the user-facing `error!()` /
176/// `warning!()` / `message!()` / `condition!()` macros (routed through
177/// [`crate::unwind_protect`]) attach their own `data`.
178///
179/// # Safety
180///
181/// Must be called from R's main thread in a valid R allocation context — the
182/// body performs raw R allocation (`Rf_allocVector`, `SET_VECTOR_ELT`,
183/// `SETATTRIB`) which is UB off the main thread. See
184/// [`make_rust_condition_value_with_data`] for the full contract.
185#[inline]
186pub unsafe fn make_rust_condition_value(
187 message: &str,
188 kind: &str,
189 class: Option<&str>,
190 call: Option<SEXP>,
191) -> SEXP {
192 let class: Vec<String> = class.map(|c| vec![c.to_string()]).unwrap_or_default();
193 // SAFETY: caller upholds the main-thread + valid-allocation-context contract.
194 unsafe {
195 make_rust_condition_value_with_data(
196 message,
197 kind,
198 &class,
199 call,
200 None,
201 ConditionCall::Inherit,
202 )
203 }
204}
205
206/// Build the tagged value for an [`RCondition`] payload (`error!()`,
207/// `warning!()`, `message!()`, `condition!()`, a queued `defer_*` entry, or a
208/// payload reconstructed across a package boundary): its kind, message,
209/// classes and data, and `call` unless the payload says
210/// [`ConditionCall::None`].
211///
212/// # Safety
213///
214/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
215/// valid allocation context.
216#[doc(hidden)]
217pub unsafe fn rust_condition_value(condition: RCondition, call: Option<SEXP>) -> SEXP {
218 let (kind, parts) = condition.into_parts();
219 // SAFETY: forwarded from the caller.
220 unsafe { condition_parts_value(kind, parts, call) }
221}
222
223/// [`make_rust_condition_value_with_data`] over the [`ErrParts`] of a
224/// condition.
225///
226/// # Safety
227///
228/// Same contract as [`make_rust_condition_value_with_data`].
229pub(crate) unsafe fn condition_parts_value(
230 kind: &str,
231 parts: ErrParts,
232 call: Option<SEXP>,
233) -> SEXP {
234 // SAFETY: forwarded from the caller.
235 unsafe {
236 make_rust_condition_value_with_data(
237 &parts.message,
238 kind,
239 &parts.class,
240 call,
241 parts.data,
242 parts.call,
243 )
244 }
245}
246
247/// Build the tagged value for a `Result<T, E>::Err` from the parts the
248/// generated wrapper probed off `e` (see
249/// [`crate::__mx_result_err_parts!`]): `kind = "result_err"`, the error's
250/// class vector (empty for the plain `Debug` fallback), its structured
251/// fields, and no call when its [`RConditionData::call`](crate::condition::RConditionData::call)
252/// is [`ConditionCall::None`] (an [`RError::without_call`](crate::condition::RError::without_call)).
253///
254/// # Safety
255///
256/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
257/// valid allocation context. Every generated `Err` arm runs inside the
258/// wrapper's `with_r_unwind_protect` closure, which satisfies it.
259pub unsafe fn result_err_condition_value(parts: ErrParts, call: Option<SEXP>) -> SEXP {
260 // SAFETY: forwarded from the caller.
261 unsafe { condition_parts_value(kind::RESULT_ERR, parts, call) }
262}
263
264/// Build the tagged value for an argument that failed its Rust-side
265/// conversion: `kind = "conversion"`, the parts probed off the error by
266/// [`crate::__mx_conversion_err_parts!`] (class, message and data from an
267/// [`RConditionData`](crate::condition::RConditionData) impl, the R-worded
268/// reason for a built-in conversion error, else the `Display` text), with
269/// `prefix` before the message (`'x' must be a single integer`, or
270/// `invalid 'x' argument` when the macro has no R-facing expectation for the
271/// type), the crate's `conversion_error_class` (`crate_class`, emitted by the
272/// macro from `[package.metadata.miniextendr]`) after the error's own
273/// classes, the parameter's R name as `e$param` and the Rust type as
274/// `e$rust_type`. `arg_message`, when the error words the whole argument
275/// error itself ([`crate::__mx_conversion_arg_message!`]: a `match_arg`
276/// choice error, in the words of the R-side `match_arg` check, #1767), is
277/// the message instead of `<prefix>: <reason>`. See
278/// [`crate::condition::conversion_err_parts`] for how they combine.
279///
280/// The R-side argument checks raise the same condition from R
281/// (`.miniextendr_arg_error`, #1591), so a handler for the crate class, or
282/// for `rust_error`, sees every argument error whichever side caught it.
283///
284/// # Safety
285///
286/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
287/// valid allocation context. Every generated conversion `Err` arm runs inside
288/// the wrapper's `with_r_unwind_protect` closure, which satisfies it.
289pub unsafe fn conversion_condition_value(
290 prefix: &str,
291 param: &str,
292 rust_type: Option<&str>,
293 crate_class: &[&str],
294 parts: ErrParts,
295 arg_message: Option<String>,
296 call: Option<SEXP>,
297) -> SEXP {
298 let parts = crate::condition::conversion_err_parts(
299 prefix,
300 param,
301 rust_type,
302 crate_class,
303 parts,
304 arg_message,
305 );
306 // SAFETY: forwarded from the caller.
307 unsafe { condition_parts_value(kind::CONVERSION, parts, call) }
308}
309
310/// Build the tagged value for an argument check that failed in Rust after
311/// the conversion: `#[miniextendr(no_na)]` on a value its type reads as
312/// missing where R's `anyNA()` saw none (the text `"NA"` or a blank string
313/// for `AsNumeric*`, a factor `NA` level for `AsCharacter*`, ...), or on an
314/// `Either`, which has no R guard, an `NA` the arm taken refuses.
315///
316/// The condition is the one `no_na`'s R guard raises
317/// (`.miniextendr_arg_error`): `kind = "conversion"`, `message` as given,
318/// the crate's `conversion_error_class` (`crate_class`) before `rust_error`,
319/// and `e$param`. See [`crate::condition::arg_check_parts`].
320///
321/// This is the generated code's form of the argument error: it returns the
322/// value from the conversion arm and takes the classes the macro read from
323/// `[package.metadata.miniextendr]` at expansion time. A function body raises
324/// the same condition with the safe [`crate::arg_error!`] instead, whose
325/// classes the generated R wrapper fills in (#1740).
326///
327/// # Safety
328///
329/// Same contract as [`make_rust_condition_value_with_data`]: R main thread,
330/// valid allocation context. The generated check runs right after the
331/// argument's conversion, where the conversion `Err` arms run, which
332/// satisfies it.
333pub unsafe fn arg_check_condition_value(
334 message: &str,
335 param: &str,
336 crate_class: &[&str],
337 call: Option<SEXP>,
338) -> SEXP {
339 let parts = crate::condition::arg_check_parts(message, param, crate_class);
340 // SAFETY: forwarded from the caller.
341 unsafe { condition_parts_value(kind::CONVERSION, parts, call) }
342}
343
344/// Build a tagged condition-value SEXP for transport across the Rust→R boundary.
345///
346/// Used for all Rust-origin failures and user-facing conditions. The R-side
347/// switch in `condition_check_lines` reads `.val$kind` to select the condition
348/// type and `.val$class` to prepend optional user classes before the standard
349/// `rust_*` layering.
350///
351/// # Safety
352///
353/// Must be called from R's main thread in a valid R allocation context
354/// (standard R API constraint): the body performs raw R allocation
355/// (`Rf_allocVector`, `SET_VECTOR_ELT`, `SETATTRIB`) which is UB off the main
356/// thread. In practice every caller reaches this through
357/// [`crate::unwind_protect::with_r_unwind_protect`] (or a proc-macro-generated
358/// panic handler), both of which run on the main thread.
359/// The returned SEXP is unprotected — caller must protect if needed.
360///
361/// # PROTECT discipline
362///
363/// Every fresh allocation (msg, kind, optional class, the no-call marker,
364/// true-marker, and — when present — the `data` VECSXP, its names, and each
365/// field value) is added to a
366/// single [`ProtectScope`](crate::ProtectScope) before the next allocation
367/// that might trigger a GC barrier. The scope releases them all together
368/// (`UNPROTECT`) when it drops at function exit, on every branch — the RAII
369/// equivalent of the former hand-counted `prot` / `Rf_unprotect(prot)` ladder.
370/// This discipline was established by PR #344 commit `af6b4875` to fix a
371/// `recursive gc invocation` segfault on R-devel.
372///
373/// # Arguments
374///
375/// * `message` - Human-readable condition message
376/// * `kind` - Condition kind — one of the constants in [`kind`].
377/// * `class` - User-supplied classes to prepend to the layered vector, most
378/// specific first (`c(<class…>, "rust_error", …)`); empty for none.
379/// * `call` - Optional R call SEXP for error context. When `None`, uses `R_NilValue`.
380/// * `data` - Optional named condition-data payload (from the macros' `data =
381/// ...` form). When `Some`, each `(name, value)` becomes a named element of a
382/// list stored in slot `[4]`; the R helper splices these into the condition
383/// object so handlers can read `e$<name>`. When `None`, slot `[4]` is `NULL`.
384/// * `call_choice` - [`ConditionCall::None`] writes the "no call" marker,
385/// `FALSE`, into slot `[3]` instead of `call`: the R helper then signals the
386/// condition with `call = NULL` rather than falling back to the wrapper's
387/// call, and [`RCondition::from_tagged_sexp`] reads the choice back across
388/// a package boundary.
389pub unsafe fn make_rust_condition_value_with_data(
390 message: &str,
391 kind: &str,
392 class: &[String],
393 call: Option<SEXP>,
394 data: Option<crate::condition::ConditionData>,
395 call_choice: ConditionCall,
396) -> SEXP {
397 unsafe {
398 // PROTECT discipline: every fresh allocation that's live across another
399 // allocation must be protected. SET_VECTOR_ELT and SETATTRIB can both
400 // trigger old-to-new GC barriers; R-devel's GC fires more aggressively
401 // here than R-release/oldrel, so unprotected intermediates corrupt the
402 // heap on R-devel even when R 4.5/4.4 happen to survive (PR #344 fix).
403 // A single ProtectScope roots every intermediate; it releases them all
404 // when it drops at function exit.
405 let scope = crate::ProtectScope::new();
406 let list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, 5));
407
408 // Element 0: error message
409 let msg_cstr = to_cstring_lossy(message, "<invalid error message>");
410 let msg_charsxp = sys::Rf_mkCharCE(msg_cstr.as_ptr(), CE_UTF8);
411 let msg_sexp = scope.protect_raw(SEXP::scalar_string(msg_charsxp));
412 list.set_vector_elt(0, msg_sexp);
413
414 // Element 1: kind string
415 let kind_cstr = to_cstring_lossy(kind, kind::OTHER_RUST_ERROR);
416 let kind_charsxp = sys::Rf_mkCharCE(kind_cstr.as_ptr(), CE_UTF8);
417 let kind_sexp = scope.protect_raw(SEXP::scalar_string(kind_charsxp));
418 list.set_vector_elt(1, kind_sexp);
419
420 // Element 2: optional custom class vector (NULL when empty). The STRSXP
421 // is protected before its CHARSXPs are allocated; the R helper splices
422 // it with `c(.class, "rust_error", ...)`, so any length works.
423 let class_sexp = if class.is_empty() {
424 SEXP::nil()
425 } else {
426 let n: isize = class
427 .len()
428 .try_into()
429 .expect("condition class count exceeds isize::MAX");
430 let class_vec = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::STRSXP, n));
431 for (i, class_name) in class.iter().enumerate() {
432 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
433 let class_cstr = to_cstring_lossy(class_name, "rust_condition");
434 let class_charsxp = sys::Rf_mkCharCE(class_cstr.as_ptr(), CE_UTF8);
435 class_vec.set_string_elt(idx, class_charsxp);
436 }
437 class_vec
438 };
439 list.set_vector_elt(2, class_sexp);
440
441 // Element 3: the call. A caller-owned SEXP (already protected) or
442 // R_NilValue; for a call-less condition the "no call" marker, a fresh
443 // FALSE LGLSXP, protected until the list roots it.
444 let call_sexp = match call_choice {
445 ConditionCall::Inherit => call.unwrap_or(SEXP::nil()),
446 ConditionCall::None => scope.protect_raw(SEXP::scalar_logical(false)),
447 };
448 list.set_vector_elt(3, call_sexp);
449
450 // Element 4: optional named-list condition data (NULL when absent).
451 //
452 // PROTECT discipline: we build a fresh VECSXP `data_list` plus a STRSXP
453 // `data_names`, and each field's materialised SEXP. Every one of these
454 // is live across subsequent allocations, so each is protected before
455 // the next alloc:
456 // - data_list protected before any field materialisation,
457 // - data_names protected before per-field CHARSXP allocations,
458 // - each field value materialised then immediately stored into the
459 // protected data_list (so it is rooted by the list before the next
460 // field allocates — same shape as `List::from_pairs`).
461 let data_sexp = if let Some(fields) = data {
462 let n: isize = fields
463 .len()
464 .try_into()
465 .expect("condition data length exceeds isize::MAX");
466 let data_list = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::VECSXP, n));
467 let data_names = scope.protect_raw(sys::Rf_allocVector(SEXPTYPE::STRSXP, n));
468 for (i, (name, value)) in fields.into_iter().enumerate() {
469 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
470 // Materialise the value and immediately root it in data_list
471 // (protected) before the name CHARSXP allocation below.
472 let value_sexp = value.into_sexp();
473 data_list.set_vector_elt(idx, value_sexp);
474 let name_cstr = to_cstring_lossy(&name, "<invalid name>");
475 let name_charsxp = sys::Rf_mkCharCE(name_cstr.as_ptr(), CE_UTF8);
476 data_names.set_string_elt(idx, name_charsxp);
477 }
478 data_list.set_names(data_names);
479 data_list
480 } else {
481 SEXP::nil()
482 };
483 list.set_vector_elt(4, data_sexp);
484
485 // Names / class symbols are cached. The TRUE marker on set_attr is a
486 // fresh LGLSXP — protect across the SETATTRIB call.
487 list.set_names(condition_names_sexp());
488 list.set_class(rust_condition_class_sexp());
489 let true_marker = scope.protect_raw(SEXP::scalar_logical(true));
490 list.set_attr(rust_condition_attr_symbol(), true_marker);
491
492 list
493 }
494}