Expand description
Tagged condition value transport.
Rust-origin failures (panics, Result::Err, Option::None) and user-raised
conditions (error!(), warning!(), message!(), condition!()) are
converted to a tagged SEXP value instead of raising an R error immediately.
The generated R wrapper inspects this tagged value and escalates it to a
proper R condition past the Rust boundary, with rust_* class layering.
§Why tagged SEXP instead of Rf_error
The naive way to surface a Rust error in R is to call Rf_error, which
longjmps out of the call frame. That works for C — C has no destructors —
but in Rust it skips every drop on the stack: open files, Mutex guards,
Box::into_raw round-trips, the worker-thread continuation token. Anything
holding a resource leaks or corrupts.
The framework instead catches every Rust panic (and every RCondition
panic_any payload) at the boundary inside
crate::unwind_protect::with_r_unwind_protect, encodes it as the
4-element list described below, and returns that SEXP normally. The
generated R wrapper then re-raises with stop(structure(..., class = c("rust_*", ...))). Destructors run; tryCatch sees the right class.
There is one accepted leak: on the R-longjmp branch inside
with_r_unwind_protect (when an R-origin error is propagated through via
R_ContinueUnwind), the RErrorMarker panic payload — about 8 bytes plus
Box header — escapes Rust drop ordering. This is the price we pay for
routing Rust failures through real R conditions instead of
Rf_error-via-longjmp, and is exactly why lint MXL300 forbids direct
Rf_error / Rf_errorcall in user code: every Rf_error skips Rust
destructors unconditionally, not just on the (rare) R-longjmp path.
§The three error-emission entry points
Authors of #[miniextendr] functions reach for one of:
panic!(msg)— escape hatch. Produceskind = "panic"and R classc("rust_error", "simpleError", "error", "condition"). Use for true bugs / impossible states; the caller has nothing to catch by class.miniextendr_api::error!("msg")— typed condition. Produceskind = "error"and the samerust_errorclass layering. Theclass = "my_class"form prepends a user class, giving R-sidec("rust_my_class", "rust_error", "simpleError", "error", "condition")— exactly what a caller’stryCatch(my_class = …)matches on. The siblingcrate::warning!,crate::message!,crate::condition!macros cover the non-error condition kinds.Result<_, E>whereE: std::error::Error, often viacrate::condition::AsRError— value-style propagation through Rust code. Converts at the boundary usingkind = "result_err".Option::Nonefollows the same path withkind = "none_err".
§error_in_r is the default
For every #[miniextendr] fn / method, the proc macro emits a wrapper that
routes through this tagged-SEXP transport — i.e. error_in_r = true is the
default. The opt-outs are documented on the macro:
#[miniextendr(no_error_in_r)]— bypass the tagged-SEXP path entirely. Useful for trait-ABI vtable shims and benchmarks; Rust panics become classicRf_errorlongjmps. Drops the leak above at the cost of skipping Rust destructors universally.#[miniextendr(unwrap_in_r)]—Result<T, E>returns are unwrapped on the R side rather than encoded askind = "result_err". Orthogonal to the transport: still rides this SEXP path, just changes howErris stringified.
Older comments suggesting Rf_error is the user-facing path predate PR
#344 and are wrong. The wrapper preambles now consistently use this
transport.
§Condition value structure (make_rust_condition_value)
The tagged SEXP is a 5-element named list:
error: error message (character scalar)kind: condition kind string — one of the constants inkindclass: optional user-supplied custom class (character scalar orNULL)call: the R call SEXP (orNULLif not available)data: optional named-list condition-data payload (from the macros’data = ...form), orNULL. The R helper splices these named fields into the condition object so handlers can reade$<name>.- class attribute:
"rust_condition_value" __rust_condition__attribute:TRUE
§PROTECT discipline (read before editing)
make_rust_condition_value allocates SEXPs that must remain live
across subsequent allocations (SET_VECTOR_ELT / SETATTRIB both
trigger old-to-new GC barriers): the list itself, the message scalar
STRSXP, the kind scalar STRSXP, the optional class scalar STRSXP, the
TRUE marker LGLSXP, and — when a data payload is present — the data
VECSXP, its names STRSXP, and each materialised field value. Each is
Rf_protected before the next allocation; prot counts them;
Rf_unprotect(prot) balances at exit on every branch. Field values are
materialised one at a time and rooted into the protected data list
immediately (same shape as List::from_pairs) so an unrooted value SEXP
never survives across the next allocation.
R-devel runs a more aggressive GC than R-release/oldrel and will fire
inside the window between two allocations. PR #344 commit af6b4875
tracked down a recursive gc invocation segfault that lit up only on
R-devel because the pre-existing 3-element version was lucky-not-safe;
adding the class slot crossed the threshold. If you add another fresh
allocation, protect it. A green R-release CI run is not proof of
safety here; run gctorture(TRUE) on R-devel before merging.
Modules§
- kind
- Canonical kind strings for tagged condition values.
Functions§
- make_
rust_ condition_ value - Build a tagged condition value with no structured
datapayload. - make_
rust_ condition_ value_ with_ data - Build a tagged condition-value SEXP for transport across the Rust→R boundary.
- to_
cstring_ 🔒lossy - Convert a
&strto aCString, falling back tofallbackon interior NUL bytes.