1//! Unified FFI guard for catching panics at Rust-R boundaries.
2//!
3//! Four modules independently catch panics at FFI boundaries: `worker.rs`,
4//! `altrep_bridge.rs`, `unwind_protect.rs`, and `connection.rs`. This module
5//! extracts the common pattern into a single `guarded_ffi_call` function.
6//!
7//! Most user code never calls anything here. The proc-macro layer
8//! (`#[miniextendr]`) inserts the right guard at every Rust → R boundary it
9//! generates. Reach for these helpers when you're writing a callback or
10//! trampoline that the macros don't already cover (custom connections,
11//! manual ALTREP, raw FFI shims).
12//!
13//! ## Guard Modes
14//!
15//! - [`GuardMode::CatchUnwind`]: Wraps the closure in `catch_unwind`. On panic,
16//! fires telemetry and raises an R error via `Rf_error` (diverges).
17//! Used by ALTREP `RustUnwind` callbacks.
18//!
19//! - [`GuardMode::RUnwind`]: Uses `R_UnwindProtect` to catch both Rust panics
20//! and R longjmps. Used by ALTREP callbacks that call R APIs. Routes
21//! through `crate::unwind_protect::with_r_unwind_protect_sourced`
22//! (crate-private).
23//!
24//! The ALTREP-specific `Unsafe` mode (no protection at all) stays in
25//! `altrep_bridge.rs` since it has no general applicability.
26//!
27//! ## Tradeoffs vs raising R errors directly
28//!
29//! Don't reach for `Rf_error` / `Rf_errorcall` to fail out of a callback —
30//! the longjmp skips Rust destructors and the lint **MXL300** rejects it.
31//! Panic instead; whichever guard mode you pick converts the panic into the
32//! tagged-condition transport ([`crate::error_value`]) or, on the ALTREP
33//! `RUnwind` path, raises the same structured `rust_*` condition via the
34//! crate-private `raise_rust_condition_via_stop` helper.
35//!
36//! ## Cross references
37//!
38//! - [`crate::worker::with_r_thread`] — main-thread routing entry point.
39//! - [`crate::unwind_protect::with_r_unwind_protect`] — the user-facing R
40//! error catcher; consumed by `RUnwind` mode.
4142use std::panic::{AssertUnwindSafe, catch_unwind};
4344use crate::panic_telemetry::PanicSource;
45use crate::unwind_protect::panic_payload_to_string;
4647/// FFI guard mode controlling how panics are caught at Rust-R boundaries.
48#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GuardMode {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
GuardMode::CatchUnwind => "CatchUnwind",
GuardMode::RUnwind => "RUnwind",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for GuardMode { }
#[automatically_derived]
impl ::core::clone::Clone for GuardMode {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for GuardMode { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for GuardMode { }
#[automatically_derived]
impl ::core::cmp::PartialEq for GuardMode {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for GuardMode { }Eq)]
49pub enum GuardMode {
50/// `catch_unwind` only. On panic: fire telemetry, then `Rf_error` (diverges).
51 ///
52 /// Use when R longjmps cannot occur (the closure does not call R APIs).
53CatchUnwind,
54/// `R_UnwindProtect`. Catches both Rust panics and R longjmps.
55 ///
56 /// Use when the closure may call R APIs that can error.
57RUnwind,
58}
5960/// Execute `f` inside an FFI guard selected by `mode`.
61///
62/// On panic:
63/// - Extracts the panic message from the payload.
64/// - Fires [`crate::panic_telemetry`] with `source`.
65/// - For [`GuardMode::CatchUnwind`]: raises R error via `Rf_error` (diverges — never returns).
66/// - For [`GuardMode::RUnwind`]: delegates to `with_r_unwind_protect_sourced`.
67///
68/// Deferred conditions signal on R's main thread before return (or panic
69/// conversion). Use this boundary on the main thread. Any SEXP held in the
70/// generic result must be rooted, e.g. by returning `OwnedProtect`.
71///
72/// # Parameters
73///
74/// - `f`: The closure to execute.
75/// - `mode`: Which guard strategy to use.
76/// - `source`: Attribution for telemetry if a panic occurs.
77///
78/// # Note on `fallback`
79///
80/// `GuardMode::CatchUnwind` diverges on panic (`Rf_error` never returns), so no
81/// fallback value is needed. If you need a fallback (e.g. connection trampolines
82/// that must return a value on panic without calling R), use
83/// [`guarded_ffi_call_with_fallback`] instead.
84#[inline]
85pub fn guarded_ffi_call<F, R>(f: F, mode: GuardMode, source: PanicSource) -> R
86where
87F: FnOnce() -> R,
88{
89match mode {
90 GuardMode::CatchUnwind => {
91let mark = crate::deferred_condition::mark();
92let boundary = crate::unwind_protect::Boundary::enter();
93let outcome = catch_unwind(AssertUnwindSafe(f)).map_err(|payload| {
94// An R exit carried out of an evaluation (#1835) continues in R.
95 // SAFETY: a callback boundary on R's main thread.
96let payload =
97unsafe { crate::unwind_protect::resume_if_r_unwind(payload, mark, &boundary) };
98crate::unwind_protect::panic_message_with_location(payload.as_ref())
99 });
100drop(boundary);
101match crate::deferred_condition::finish_guarded(mark, outcome) {
102Ok(val) => val,
103Err(msg) => {
104crate::panic_telemetry::fire(&msg, source);
105crate::error::r_stop(&msg)
106 }
107 }
108 }
109 GuardMode::RUnwind => crate::unwind_protect::with_r_unwind_protect_sourced(f, None, source),
110 }
111}
112113/// Execute `f` inside a `CatchUnwind` guard, returning `fallback` on panic.
114///
115/// Unlike [`guarded_ffi_call`] with `CatchUnwind` (which diverges via `Rf_error`),
116/// this variant returns the `fallback` value instead of raising an R error.
117/// This is needed for connection trampolines where panicking through R/C frames
118/// is UB but raising an R error is also undesirable (the caller expects a return
119/// value indicating failure).
120///
121/// Deferred conditions are signalled on R's main thread after the callback.
122/// R handlers may exit (including `options(warn = 2)`); the Rust panic fallback
123/// is unchanged. Finalizer callbacks must suppress conditions instead.
124/// Telemetry is fired before returning the fallback.
125#[inline]
126pub fn guarded_ffi_call_with_fallback<F, R>(f: F, fallback: R, source: PanicSource) -> R
127where
128F: FnOnce() -> R,
129{
130let mark = crate::deferred_condition::mark();
131let boundary = crate::unwind_protect::Boundary::enter();
132let outcome = match catch_unwind(AssertUnwindSafe(f)) {
133Ok(value) => Ok(value),
134Err(payload) => {
135let payload = match payload.downcast::<crate::unwind_protect::RUnwind>() {
136// An R exit carried out of an evaluation (#1835) continues in R.
137Ok(exit) if exit.belongs_to(&boundary) => {
138drop(fallback);
139crate::deferred_condition::discard(mark);
140boundary.leave();
141// SAFETY: a callback boundary on R's main thread.
142unsafe { (*exit).resume() }
143 }
144Ok(exit) => exit.stray(),
145Err(payload) => payload,
146 };
147Err(panic_payload_to_string(payload.as_ref()).into_owned())
148 }
149 };
150drop(boundary);
151let (outcome, fallback) = crate::deferred_condition::finish_guarded(mark, (outcome, fallback));
152match outcome {
153Ok(val) => val,
154Err(msg) => {
155crate::panic_telemetry::fire(&msg, source);
156fallback157 }
158 }
159}
160161#[cfg(test)]
162mod tests {
163use super::*;
164165#[test]
166fn catch_unwind_returns_value_on_success() {
167let result = guarded_ffi_call(|| 42, GuardMode::CatchUnwind, PanicSource::Worker);
168assert_eq!(result, 42);
169 }
170171#[test]
172fn fallback_returns_value_on_success() {
173let result = guarded_ffi_call_with_fallback(|| 42, -1, PanicSource::Connection);
174assert_eq!(result, 42);
175 }
176177#[test]
178fn fallback_returns_fallback_on_panic() {
179let result = guarded_ffi_call_with_fallback(|| panic!("boom"), -1, PanicSource::Connection);
180assert_eq!(result, -1);
181 }
182183#[test]
184fn fallback_fires_telemetry_on_panic() {
185use std::sync::atomic::{AtomicBool, Ordering};
186187let fired = std::sync::Arc::new(AtomicBool::new(false));
188let fired_clone = fired.clone();
189190crate::panic_telemetry::set_panic_telemetry_hook(move |report| {
191assert_eq!(report.source, PanicSource::Connection);
192assert!(report.message.contains("test panic"));
193 fired_clone.store(true, Ordering::SeqCst);
194 });
195196let _ =
197 guarded_ffi_call_with_fallback(|| panic!("test panic"), 0i32, PanicSource::Connection);
198199assert!(fired.load(Ordering::SeqCst), "telemetry hook should fire");
200crate::panic_telemetry::clear_panic_telemetry_hook();
201 }
202}