miniextendr_api/ffi_guard.rs
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//! ## Guard Modes
8//!
9//! - [`GuardMode::CatchUnwind`]: Wraps the closure in `catch_unwind`. On panic,
10//! fires telemetry and raises an R error via `Rf_error` (diverges).
11//! Used by worker and connection trampolines.
12//!
13//! - [`GuardMode::RUnwind`]: Uses `R_UnwindProtect` to catch both Rust panics
14//! and R longjmps. Used by ALTREP callbacks that call R APIs.
15//!
16//! The ALTREP-specific `Unsafe` mode (no protection at all) stays in
17//! `altrep_bridge.rs` since it has no general applicability.
18
19use std::panic::{AssertUnwindSafe, catch_unwind};
20
21use crate::panic_telemetry::PanicSource;
22use crate::unwind_protect::panic_payload_to_string;
23
24/// FFI guard mode controlling how panics are caught at Rust-R boundaries.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum GuardMode {
27 /// `catch_unwind` only. On panic: fire telemetry, then `Rf_error` (diverges).
28 ///
29 /// Use when R longjmps cannot occur (the closure does not call R APIs).
30 CatchUnwind,
31 /// `R_UnwindProtect`. Catches both Rust panics and R longjmps.
32 ///
33 /// Use when the closure may call R APIs that can error.
34 RUnwind,
35}
36
37/// Execute `f` inside an FFI guard selected by `mode`.
38///
39/// On panic:
40/// - Extracts the panic message from the payload.
41/// - Fires [`crate::panic_telemetry`] with `source`.
42/// - For [`GuardMode::CatchUnwind`]: raises R error via `Rf_error` (diverges — never returns).
43/// - For [`GuardMode::RUnwind`]: delegates to `with_r_unwind_protect_sourced`.
44///
45/// # Parameters
46///
47/// - `f`: The closure to execute.
48/// - `mode`: Which guard strategy to use.
49/// - `source`: Attribution for telemetry if a panic occurs.
50///
51/// # Note on `fallback`
52///
53/// `GuardMode::CatchUnwind` diverges on panic (`Rf_error` never returns), so no
54/// fallback value is needed. If you need a fallback (e.g. connection trampolines
55/// that must return a value on panic without calling R), use
56/// [`guarded_ffi_call_with_fallback`] instead.
57#[inline]
58pub fn guarded_ffi_call<F, R>(f: F, mode: GuardMode, source: PanicSource) -> R
59where
60 F: FnOnce() -> R,
61{
62 match mode {
63 GuardMode::CatchUnwind => match catch_unwind(AssertUnwindSafe(f)) {
64 Ok(val) => val,
65 Err(payload) => {
66 let msg = panic_payload_to_string(payload.as_ref());
67 crate::panic_telemetry::fire(&msg, source);
68 crate::error::r_stop(&msg)
69 }
70 },
71 GuardMode::RUnwind => crate::unwind_protect::with_r_unwind_protect_sourced(f, None, source),
72 }
73}
74
75/// Execute `f` inside a `CatchUnwind` guard, returning `fallback` on panic.
76///
77/// Unlike [`guarded_ffi_call`] with `CatchUnwind` (which diverges via `Rf_error`),
78/// this variant returns the `fallback` value instead of raising an R error.
79/// This is needed for connection trampolines where panicking through R/C frames
80/// is UB but raising an R error is also undesirable (the caller expects a return
81/// value indicating failure).
82///
83/// Telemetry is fired before returning the fallback.
84#[inline]
85pub fn guarded_ffi_call_with_fallback<F, R>(f: F, fallback: R, source: PanicSource) -> R
86where
87 F: FnOnce() -> R,
88{
89 match catch_unwind(AssertUnwindSafe(f)) {
90 Ok(val) => val,
91 Err(payload) => {
92 let msg = panic_payload_to_string(payload.as_ref());
93 crate::panic_telemetry::fire(&msg, source);
94 fallback
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn catch_unwind_returns_value_on_success() {
105 let result = guarded_ffi_call(|| 42, GuardMode::CatchUnwind, PanicSource::Worker);
106 assert_eq!(result, 42);
107 }
108
109 #[test]
110 fn fallback_returns_value_on_success() {
111 let result = guarded_ffi_call_with_fallback(|| 42, -1, PanicSource::Connection);
112 assert_eq!(result, 42);
113 }
114
115 #[test]
116 fn fallback_returns_fallback_on_panic() {
117 let result = guarded_ffi_call_with_fallback(|| panic!("boom"), -1, PanicSource::Connection);
118 assert_eq!(result, -1);
119 }
120
121 #[test]
122 fn fallback_fires_telemetry_on_panic() {
123 use std::sync::atomic::{AtomicBool, Ordering};
124
125 let fired = std::sync::Arc::new(AtomicBool::new(false));
126 let fired_clone = fired.clone();
127
128 crate::panic_telemetry::set_panic_telemetry_hook(move |report| {
129 assert_eq!(report.source, PanicSource::Connection);
130 assert!(report.message.contains("test panic"));
131 fired_clone.store(true, Ordering::SeqCst);
132 });
133
134 let _ =
135 guarded_ffi_call_with_fallback(|| panic!("test panic"), 0i32, PanicSource::Connection);
136
137 assert!(fired.load(Ordering::SeqCst), "telemetry hook should fire");
138 crate::panic_telemetry::clear_panic_telemetry_hook();
139 }
140}