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//! This ensures Rust destructors run cleanly before R sees the error.
10//!
11//! # Condition value structure (`make_rust_condition_value`)
12//!
13//! The tagged SEXP is a 4-element named list:
14//! - `error`: error message (character scalar)
15//! - `kind`: condition kind string — one of the constants in [`kind`]
16//! - `class`: optional user-supplied custom class (character scalar or `NULL`)
17//! - `call`: the R call SEXP (or `NULL` if not available)
18//! - class attribute: `"rust_condition_value"`
19//! - `__rust_condition__` attribute: `TRUE`
20
21use crate::cached_class::{
22 condition_names_sexp, rust_condition_attr_symbol, rust_condition_class_sexp,
23};
24use crate::ffi::{self, SEXP, SexpExt};
25
26/// Canonical kind strings for tagged condition values.
27///
28/// These constants are emitted into the `kind` slot of
29/// [`make_rust_condition_value`] and consumed by the R-side
30/// `.miniextendr_raise_condition` switch (see
31/// `registry::write_r_wrappers_to_file`). Reference these constants
32/// from codegen and runtime sites instead of bare string literals so a
33/// typo cannot silently change which switch arm fires.
34///
35/// The constants are kept in lockstep with the generated R helper; if a new
36/// kind is added, both the emission site and the R helper need to learn it.
37pub mod kind {
38 /// Default kind for Rust panics that surface to R via the generic panic
39 /// path (no `RCondition` payload). Layered as `rust_error`.
40 pub const PANIC: &str = "panic";
41 /// `Result<_, E>::Err(...)` formatted via `Debug` (raised when the user
42 /// returns an `Err` from a `#[miniextendr]` fn/method).
43 pub const RESULT_ERR: &str = "result_err";
44 /// `Option<T>::None` reached where a value was required (raised by the
45 /// `NoneOnErr` / required-Option return paths).
46 pub const NONE_ERR: &str = "none_err";
47 /// `TryFromSexp` / coerce / strict-mode conversion failed at argument
48 /// unmarshalling.
49 pub const CONVERSION: &str = "conversion";
50 /// User-raised `error!(...)` condition.
51 pub const ERROR: &str = "error";
52 /// User-raised `warning!(...)` condition.
53 pub const WARNING: &str = "warning";
54 /// User-raised `message!(...)` condition.
55 pub const MESSAGE: &str = "message";
56 /// User-raised `condition!(...)` condition.
57 pub const CONDITION: &str = "condition";
58 /// Fallback kind written by [`super::make_rust_condition_value`] when the
59 /// caller's `kind` argument contained an interior NUL and could not be
60 /// converted to a `CString`. Should not appear in normal flow; the match
61 /// arm in [`crate::condition::RCondition::from_sexp`] handles it
62 /// defensively by degrading to `RCondition::Error`.
63 pub const OTHER_RUST_ERROR: &str = "other_rust_error";
64}
65
66/// Convert a `&str` to a `CString`, falling back to `fallback` on interior NUL bytes.
67///
68/// Used internally by [`make_rust_condition_value`] to avoid duplicating the
69/// `CString::new(s).unwrap_or_else(…)` pattern across every slot.
70fn to_cstring_lossy(s: &str, fallback: &str) -> std::ffi::CString {
71 std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new(fallback).unwrap())
72}
73
74/// Build a tagged condition-value SEXP for transport across the Rust→R boundary.
75///
76/// Used for all Rust-origin failures and user-facing conditions. The R-side
77/// switch in `condition_check_lines` reads `.val$kind` to select the condition
78/// type and `.val$class` to prepend optional user classes before the standard
79/// `rust_*` layering.
80///
81/// # Safety
82///
83/// Must be called from R's main thread (standard R API constraint).
84/// The returned SEXP is unprotected — caller must protect if needed.
85///
86/// # PROTECT discipline
87///
88/// Every fresh allocation (msg, kind, optional class, true-marker) is protected
89/// before the next allocation that might trigger a GC barrier. The `prot` counter
90/// is incremented on each `Rf_protect` and balanced by `Rf_unprotect(prot)` at
91/// exit on all branches. This pattern was established by PR #344 commit `af6b4875`
92/// to fix a `recursive gc invocation` segfault on R-devel.
93///
94/// # Arguments
95///
96/// * `message` - Human-readable condition message
97/// * `kind` - Condition kind — one of the constants in [`kind`].
98/// * `class` - Optional user-supplied class name to prepend to the layered vector
99/// * `call` - Optional R call SEXP for error context. When `None`, uses `R_NilValue`.
100pub fn make_rust_condition_value(
101 message: &str,
102 kind: &str,
103 class: Option<&str>,
104 call: Option<SEXP>,
105) -> SEXP {
106 unsafe {
107 // PROTECT discipline: every fresh allocation that's live across another
108 // allocation must be protected. SET_VECTOR_ELT and SETATTRIB can both
109 // trigger old-to-new GC barriers; R-devel's GC fires more aggressively
110 // here than R-release/oldrel, so unprotected intermediates corrupt the
111 // heap on R-devel even when R 4.5/4.4 happen to survive (PR #344 fix).
112 let list = ffi::Rf_allocVector(ffi::SEXPTYPE::VECSXP, 4);
113 ffi::Rf_protect(list);
114 let mut prot = 1;
115
116 // Element 0: error message
117 let msg_cstr = to_cstring_lossy(message, "<invalid error message>");
118 let msg_charsxp = ffi::Rf_mkCharCE(msg_cstr.as_ptr(), ffi::CE_UTF8);
119 let msg_sexp = SEXP::scalar_string(msg_charsxp);
120 ffi::Rf_protect(msg_sexp);
121 prot += 1;
122 list.set_vector_elt(0, msg_sexp);
123
124 // Element 1: kind string
125 let kind_cstr = to_cstring_lossy(kind, kind::OTHER_RUST_ERROR);
126 let kind_charsxp = ffi::Rf_mkCharCE(kind_cstr.as_ptr(), ffi::CE_UTF8);
127 let kind_sexp = SEXP::scalar_string(kind_charsxp);
128 ffi::Rf_protect(kind_sexp);
129 prot += 1;
130 list.set_vector_elt(1, kind_sexp);
131
132 // Element 2: optional custom class (NULL when not provided).
133 // Only the Some-branch allocates; nil is constant.
134 let class_sexp = if let Some(class_name) = class {
135 let class_cstr = to_cstring_lossy(class_name, "rust_condition");
136 let class_charsxp = ffi::Rf_mkCharCE(class_cstr.as_ptr(), ffi::CE_UTF8);
137 let s = SEXP::scalar_string(class_charsxp);
138 ffi::Rf_protect(s);
139 prot += 1;
140 s
141 } else {
142 SEXP::nil()
143 };
144 list.set_vector_elt(2, class_sexp);
145
146 // Element 3: caller-owned SEXP — already protected (or R_NilValue)
147 list.set_vector_elt(3, call.unwrap_or(SEXP::nil()));
148
149 // Names / class symbols are cached. The TRUE marker on set_attr is a
150 // fresh LGLSXP — protect across the SETATTRIB call.
151 list.set_names(condition_names_sexp());
152 list.set_class(rust_condition_class_sexp());
153 let true_marker = SEXP::scalar_logical(true);
154 ffi::Rf_protect(true_marker);
155 prot += 1;
156 list.set_attr(rust_condition_attr_symbol(), true_marker);
157
158 ffi::Rf_unprotect(prot);
159 list
160 }
161}