Skip to main content

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}