Skip to main content

miniextendr_api/
unwind_protect.rs

1//! Safe API for R's `R_UnwindProtect`
2//!
3//! This module provides [`with_r_unwind_protect`] for handling R errors with Rust cleanup.
4//! It automatically runs Rust destructors when R errors occur.
5//!
6//! **Important**: R uses `longjmp` for error handling, which normally bypasses Rust destructors.
7//! Use this API to ensure cleanup happens even when R errors occur.
8//!
9//! ## When to reach for this
10//!
11//! - **Calling R APIs that can error from a body you wrote yourself**
12//!   (custom ALTREP, custom connection trampoline, hand-rolled FFI shim).
13//!   Wrap the R-calling section in [`with_r_unwind_protect`] so Rust
14//!   destructors run if R longjmps.
15//! - **Inside a [`with_r_unwind_protect`] body it is safe to use `*_unchecked`
16//!   variants of the R FFI** — see the [`crate::sys`] module doc. The lint
17//!   **MXL301** recognises this as one of the three contexts where bypassing
18//!   the main-thread assertion is valid (the other two being ALTREP callbacks
19//!   and [`crate::worker::with_r_thread`] bodies).
20//!
21//! ## You probably don't need this from a `#[miniextendr]` body
22//!
23//! The proc-macro already wraps every function and method in a guard that
24//! converts panics into the tagged-condition transport ([`crate::error_value`]).
25//! Returning `Result::Err`, `Option::None`, or calling `panic!()` /
26//! [`crate::error!`] / [`crate::warning!`] / [`crate::message!`] is the
27//! idiomatic path. Direct [`with_r_unwind_protect`] use inside that body is
28//! almost always wrong — you'd be nesting an `R_UnwindProtect` inside another
29//! `R_UnwindProtect`, paying the longjmp-leak cost twice (see "Leaks" below).
30//!
31//! ## Don't use `Rf_error`
32//!
33//! `Rf_error` and `Rf_errorcall` longjmp directly, skipping every Rust
34//! destructor on the stack. The lint **MXL300** forbids them in user code.
35//! Panic instead (or call [`crate::error!`]) and the framework raises the
36//! corresponding R condition for you.
37//!
38//! ## Leaks
39//!
40//! On the R longjmp path (when R unwinds out of the protected body),
41//! `with_r_unwind_protect` leaks ~8 bytes (an `RErrorMarker` + `Box` header)
42//! because the cleanup handler can't reclaim them via
43//! `Box::from_raw`. Regular Rust panics from inside the body don't leak.
44//! This is the cost MXL300 is buying off: every direct `Rf_error()` would
45//! incur the same leak with no observability.
46//!
47//! ## Log drain
48//!
49//! Every call to `with_r_unwind_protect` (and its variants) drains the
50//! cross-thread log queue via the crate-private `drain_log_queue_if_available`
51//! helper before returning or re-raising an R error. This ensures that records
52//! buffered by worker threads are flushed to R's console on every FFI exit —
53//! including error paths.
54//!
55//! ## Cross references
56//!
57//! - [`crate::worker::with_r_thread`] — routes a closure to R's main thread.
58//! - [`crate::ffi_guard`] — unified panic-catching trampoline that consumes
59//!   `with_r_unwind_protect_sourced` for ALTREP `RUnwind` mode.
60//! - [`crate::error_value`] / [`mod@crate::condition`] — panic → R condition
61//!   transport.
62use std::{
63    any::Any,
64    borrow::Cow,
65    cell::Cell,
66    ffi::c_void,
67    panic::{AssertUnwindSafe, catch_unwind},
68    sync::OnceLock,
69};
70
71// region: raise_rust_condition_via_stop — Approach 3 for ALTREP RUnwind path
72
73/// Raise the R condition for a tagged condition value by evaluating the
74/// shared `.miniextendr_raise_condition` helper on it, the helper every
75/// generated R wrapper calls on the value its `.Call()` returns.
76///
77/// This is **Approach 3** from the issue-345 plan: an `Rf_eval` that ends in
78/// `stop(structure(...))`, for contexts with no outer R wrapper to inspect a
79/// tagged SEXP. It is the only viable option for ALTREP callbacks, which are
80/// invoked directly by R's runtime (no `.Call` frame, no R wrapper).
81///
82/// `tagged_value` builds the value (with [`crate::error_value`]'s
83/// constructors). Because the helper raises it, the condition is the one the
84/// wrapper raises for the same value, field for field (#1768): `message`,
85/// `call`, `kind`, then the `data =` fields spliced with `utils::modifyList`,
86/// so a data field named `message`, `call` or `kind` replaces the base field
87/// on both transports (#1315); the classes ahead of the `rust_*` layering.
88/// The copy evaluated here lives in the base namespace, so the caller
89/// resolves `arg_error!`'s class marker in Rust first.
90///
91/// The value must be of an error kind (`stop()` longjmps, so this function
92/// never returns); the helper's warning / message / condition arms return.
93///
94/// ## Nothing Rust-owned survives the longjmp
95///
96/// The longjmp skips every Rust frame above R's, so no destructor runs past
97/// this point. `tagged_value` consumes what it captures (message, classes,
98/// data) and is itself dropped before `Rf_eval`; the SEXPs are rooted with
99/// plain `Rf_protect`, whose stack R resets on the jump, rather than a
100/// drop guard that would never run.
101///
102/// ## MXL300 compliance
103///
104/// This function raises an R error via `Rf_eval(stop(...))`, not via direct
105/// `Rf_error`/`Rf_errorcall`. MXL300 does not flag `Rf_eval`.
106///
107/// # Safety
108///
109/// Must be called from R's main thread in a context where R longjmps are
110/// safe, with no Rust value that needs dropping left on the caller's frames.
111/// In practice, always called from `with_r_unwind_protect_sourced` (ALTREP
112/// `RUnwind` guards, [`with_r_unwind_protect_or_raise`]) after its guarded
113/// closure has returned.
114pub(crate) unsafe fn raise_rust_condition_via_stop(
115    tagged_value: impl FnOnce() -> SEXP,
116    call: Option<SEXP>,
117) -> ! {
118    use crate::sys::{R_BaseEnv, Rf_eval, Rf_lang3, Rf_protect};
119
120    unsafe {
121        let helper = crate::deferred_condition::raise_condition_helper();
122        let tagged = Rf_protect(tagged_value());
123        let expr = Rf_protect(Rf_lang3(helper, tagged, call.unwrap_or(SEXP::nil())));
124        // Longjmps through `stop()`; R pops the protect stack on the way out.
125        Rf_eval(expr, R_BaseEnv);
126    }
127    // Only a non-error tagged value (a bug in the caller) gets here, and
128    // unwinding out of an ALTREP callback would cross R's C frames.
129    std::process::abort()
130}
131
132/// The tagged value of a generic panic, `kind = "panic"`: what
133/// [`with_r_unwind_protect`] returns for one.
134fn panic_condition_value(message: &str, call: Option<SEXP>) -> SEXP {
135    // SAFETY: only called by `raise_rust_condition_via_stop`, on the main thread.
136    unsafe {
137        crate::error_value::make_rust_condition_value(
138            message,
139            crate::error_value::kind::PANIC,
140            None,
141            call,
142        )
143    }
144}
145
146// endregion
147
148use crate::sys::{self, R_ContinueUnwind, R_UnwindProtect_C_unwind};
149use crate::{Rboolean, SEXP};
150
151/// Global continuation token for R_UnwindProtect.
152///
153/// Using a single global token instead of thread-local tokens avoids leaking
154/// one token per thread that uses `with_r_unwind_protect`.
155///
156/// # Safety
157///
158/// The token is created and preserved once during first use. It remains valid
159/// for the entire R session.
160static R_CONTINUATION_TOKEN: OnceLock<SEXP> = OnceLock::new();
161
162/// Get or create the global continuation token.
163///
164/// This is public for use by the worker module.
165pub(crate) fn get_continuation_token() -> SEXP {
166    *R_CONTINUATION_TOKEN.get_or_init(|| {
167        // The continuation token must be created on R's main thread
168        // (R_MakeUnwindCont is an R API call). OnceLock ensures it is
169        // only created once and safely shared.
170        unsafe {
171            let token = sys::R_MakeUnwindCont();
172            sys::R_PreserveObject(token);
173            token
174        }
175    })
176}
177
178/// Panic payload whose message is already final (location folded, or
179/// deliberately location-free): downstream folds must use it verbatim and
180/// must NOT append the current thread's recorded panic location (#1245).
181///
182/// Produced by `worker::route_to_main_thread`'s re-panic when a `with_r_thread`
183/// closure panics on the main thread: the main-thread stringify point already
184/// folded the *true* origin location into the message before it crossed back
185/// to the worker, so the worker's own re-panic (needed to unwind out of
186/// `run_on_worker`) must carry that message forward untouched rather than
187/// re-fold its own relay call site on top.
188pub(crate) struct PreLocatedPanic(pub(crate) String);
189
190/// Extract a message from a panic payload.
191///
192/// Handles `&str`, `String`, `&String`, `PreLocatedPanic` and
193/// [`RCondition`](enum@crate::condition::RCondition) payloads (the condition's
194/// message) consistently. The borrowed variants are returned as
195/// `Cow::Borrowed`, so the common `panic!("literal")` case avoids the heap
196/// allocation that a `String` return would force. Unrecognised payload types
197/// fall back to a `Cow::Borrowed` static string.
198///
199/// Call `.into_owned()` (or `.to_string()`) at sites that need an owned
200/// `String`.
201pub fn panic_payload_to_string(payload: &(dyn Any + Send)) -> Cow<'_, str> {
202    if let Some(&s) = payload.downcast_ref::<&str>() {
203        Cow::Borrowed(s)
204    } else if let Some(s) = payload.downcast_ref::<String>() {
205        Cow::Borrowed(s.as_str())
206    } else if let Some(s) = payload.downcast_ref::<&String>() {
207        Cow::Borrowed(s.as_str())
208    } else if let Some(pre) = payload.downcast_ref::<PreLocatedPanic>() {
209        Cow::Borrowed(pre.0.as_str())
210    } else if let Some(condition) = payload.downcast_ref::<crate::condition::RCondition>() {
211        Cow::Borrowed(condition.message())
212    } else {
213        Cow::Borrowed("unknown panic")
214    }
215}
216
217/// Stringify a panic payload and fold in the Rust source location the panic
218/// hook recorded on the *current* thread, producing the final R-facing message.
219///
220/// Returns `panic_payload_to_string(payload)` with a `\n(at file:line)` suffix
221/// when [`crate::backtrace::take_last_panic_location`] has a location for this
222/// thread, otherwise the bare message. The location is captured in the process
223/// panic hook (`backtrace.rs`), which fires on the panicking thread — so this
224/// **must be called on the same thread that ran the panicking closure** (main
225/// for main-thread `#[miniextendr]` fns; the worker for worker-dispatched fns).
226///
227/// Only the *generic panic* path uses this. `error!`/`warning!`/`message!`/
228/// `condition!` (and `Result::Err` / `Option::None`) travel the typed
229/// `RCondition` / tagged-value branches and are deliberately left byte-for-byte
230/// unchanged — they carry no location suffix.
231pub(crate) fn panic_message_with_location(payload: &(dyn Any + Send)) -> String {
232    let msg = panic_payload_to_string(payload);
233    match crate::backtrace::take_last_panic_location() {
234        Some((file, line)) => ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}\n(at {1}:{2})", msg, file,
                line))
    })format!("{msg}\n(at {file}:{line})"),
235        None => msg.into_owned(),
236    }
237}
238
239// region: R exits carried as a Rust unwind (#1835)
240
241#[doc = r" The last boundary id handed out on this thread."]
const LAST_BOUNDARY: ::std::thread::LocalKey<Cell<u64>> =
    {
        const __RUST_STD_INTERNAL_INIT: Cell<u64> = { Cell::new(0) };
        unsafe {
            ::std::thread::LocalKey::new(const {
                        if ::std::mem::needs_drop::<Cell<u64>>() {
                            |__rust_std_internal_init|
                                {
                                    #[thread_local]
                                    static __RUST_STD_INTERNAL_VAL:
                                        ::std::thread::local_impl::EagerStorage<Cell<u64>> =
                                        ::std::thread::local_impl::EagerStorage::new(__RUST_STD_INTERNAL_INIT);
                                    __RUST_STD_INTERNAL_VAL.get()
                                }
                        } else {
                            |__rust_std_internal_init|
                                {
                                    #[thread_local]
                                    static __RUST_STD_INTERNAL_VAL: Cell<u64> =
                                        __RUST_STD_INTERNAL_INIT;
                                    &__RUST_STD_INTERNAL_VAL
                                }
                        }
                    })
        }
    };thread_local! {
242    /// The innermost [`Boundary`] on this thread; `0` outside any.
243    static BOUNDARY: Cell<u64> = const { Cell::new(0) };
244    /// The last boundary id handed out on this thread.
245    static LAST_BOUNDARY: Cell<u64> = const { Cell::new(0) };
246}
247
248/// A miniextendr boundary that resumes R exits ([`RUnwind`]): the innermost on
249/// its thread from [`enter`](Self::enter) until it is dropped or
250/// [`leave`](Self::leave)s.
251///
252/// An exit belongs to the boundary that was innermost when its evaluation
253/// started, and only that boundary may resume it. Its jump target is an R
254/// context on the C stack, alive only while that boundary's call is: an exit
255/// caught with `catch_unwind`, kept, and resumed after the call returned would
256/// jump into a frame that no longer exists. [`resume_if_r_unwind`] refuses it.
257pub(crate) struct Boundary {
258    id: u64,
259    outer: u64,
260}
261
262impl Boundary {
263    /// Become the innermost boundary on this thread.
264    #[inline]
265    pub(crate) fn enter() -> Self {
266        let id = LAST_BOUNDARY.with(|last| {
267            let id = last.get() + 1;
268            last.set(id);
269            id
270        });
271        let outer = BOUNDARY.with(|current| current.replace(id));
272        Boundary { id, outer }
273    }
274
275    /// Make the enclosing boundary the innermost again. Call it before leaving
276    /// by an R jump, which skips `Drop`.
277    #[inline]
278    pub(crate) fn leave(&self) {
279        BOUNDARY.with(|current| current.set(self.outer));
280    }
281}
282
283impl Drop for Boundary {
284    #[inline]
285    fn drop(&mut self) {
286        self.leave();
287    }
288}
289
290/// An R non-local exit that [`call_unwinding`] carries out of its callback
291/// as a Rust unwind: an error, a condition caught by an exiting handler set up
292/// outside the call (`tryCatch(warning = )`), a restart, an interrupt.
293///
294/// The unwind runs the destructors of the Rust frames between the evaluation
295/// and the boundary that catches it. The boundary ([`run_r_unwind_protect`],
296/// the worker's main-thread loop, the FFI guards) then resumes the exit with
297/// `R_ContinueUnwind`, and R carries on to the context it was jumping to, with
298/// the condition object and call it was jumping with.
299///
300/// The continuation token is private to one evaluation and preserved until
301/// the exit resumes, so R code that a destructor runs during the unwind (a
302/// protected call of its own, which reuses the shared token) cannot overwrite
303/// the pending jump or let the collector reclaim it (#1507).
304pub(crate) struct RUnwind {
305    /// `R_MakeUnwindCont()`, preserved: the jump target and the value R was
306    /// jumping with.
307    token: SEXP,
308    /// The [`Boundary`] that was innermost when the evaluation started.
309    boundary: u64,
310}
311
312impl RUnwind {
313    /// Whether `boundary` may resume this exit: the boundary its evaluation
314    /// ran in, still on the stack.
315    #[inline]
316    pub(crate) fn belongs_to(&self, boundary: &Boundary) -> bool {
317        self.boundary == boundary.id
318    }
319
320    /// Continue the R exit where `R_UnwindProtect` stopped it. Never returns.
321    ///
322    /// # Safety
323    ///
324    /// R's main thread, at the boundary the exit [`belongs_to`](Self::belongs_to),
325    /// after its [`Boundary::leave`], with no Rust value left to drop between
326    /// this call and the R context that owns the frame (the C entry point R
327    /// called).
328    pub(crate) unsafe fn resume(self) -> ! {
329        let token = self.token;
330        std::mem::forget(self);
331        unsafe {
332            // `R_jumpctxt` roots the value it jumps with only after the cleanups
333            // of the contexts it passes have run (it sets `R_ReturnedValue`
334            // after `R_run_onexits`), and those may allocate. The protection
335            // keeps the token, and with it the value, alive until then; the
336            // jump resets the protect stack (`R_restore_globals`), which ends it.
337            sys::Rf_protect(token);
338            sys::R_ReleaseObject(token);
339            R_ContinueUnwind(token)
340        }
341    }
342
343    /// The payload a boundary raises in place of an exit it may not resume
344    /// (see [`Boundary`]). The exit is dropped.
345    pub(crate) fn stray(self: Box<Self>) -> Box<dyn Any + Send> {
346        drop(self);
347        Box::new(
348            "an R exit (an error, a condition handler's exit or a restart) was caught with \
349             `catch_unwind` and resumed outside the call that raised it; it cannot continue",
350        )
351    }
352}
353
354impl Drop for RUnwind {
355    /// Reached when something other than the exit's boundary caught the unwind
356    /// and dropped it, or the boundary refused it ([`RUnwind::stray`]). The R
357    /// exit is abandoned: R carries on from where the evaluation stopped, as
358    /// after `R_tryEval`. Release the token, on R's main thread: dropped on
359    /// another thread (code that moved the caught payload there), it waits in
360    /// [`ABANDONED_OFF_MAIN`] for the next evaluation.
361    fn drop(&mut self) {
362        if crate::worker::is_r_main_thread() {
363            unsafe { sys::R_ReleaseObject(self.token) }
364        } else {
365            ABANDONED_OFF_MAIN
366                .lock()
367                .unwrap_or_else(std::sync::PoisonError::into_inner)
368                .push(self.token);
369            HAS_ABANDONED_OFF_MAIN.store(true, std::sync::atomic::Ordering::Release);
370        }
371    }
372}
373
374/// Tokens of R exits dropped off R's main thread, where `R_ReleaseObject`
375/// would race R. [`call_unwinding`] releases them on the main thread.
376static ABANDONED_OFF_MAIN: std::sync::Mutex<Vec<SEXP>> = std::sync::Mutex::new(Vec::new());
377
378/// Whether [`ABANDONED_OFF_MAIN`] may hold a token: the evaluation's fast path.
379static HAS_ABANDONED_OFF_MAIN: std::sync::atomic::AtomicBool =
380    std::sync::atomic::AtomicBool::new(false);
381
382/// Release the tokens of exits dropped off the main thread.
383///
384/// # Safety
385///
386/// R's main thread.
387unsafe fn release_abandoned_off_main() {
388    if HAS_ABANDONED_OFF_MAIN.swap(false, std::sync::atomic::Ordering::Acquire) {
389        let tokens = std::mem::take(
390            &mut *ABANDONED_OFF_MAIN
391                .lock()
392                .unwrap_or_else(std::sync::PoisonError::into_inner),
393        );
394        for token in tokens {
395            unsafe { sys::R_ReleaseObject(token) };
396        }
397    }
398}
399
400/// Resume `payload` if it carries an R exit ([`RUnwind`]) that belongs to
401/// `boundary`; return it otherwise, or [`RUnwind::stray`] for an exit from
402/// elsewhere. Conditions deferred since `deferred_mark` belong to the call the
403/// exit abandons and are discarded first, as on the R-error path.
404///
405/// # Safety
406///
407/// As [`RUnwind::resume`]: R's main thread, at `boundary`, with nothing left
408/// to drop above the C entry point.
409pub(crate) unsafe fn resume_if_r_unwind(
410    payload: Box<dyn Any + Send>,
411    deferred_mark: usize,
412    boundary: &Boundary,
413) -> Box<dyn Any + Send> {
414    match payload.downcast::<RUnwind>() {
415        Ok(exit) if exit.belongs_to(boundary) => {
416            crate::deferred_condition::discard(deferred_mark);
417            boundary.leave();
418            unsafe { exit.resume() }
419        }
420        Ok(exit) => exit.stray(),
421        Err(payload) => payload,
422    }
423}
424
425/// Evaluate `expr` in `env` with `Rf_eval`, in the caller's R context: the
426/// condition handlers and restarts established around the `.Call()` stay in
427/// place, unlike under `R_tryEval`, whose `R_ToplevelExec` empties the handler
428/// and restart stacks for the duration. An R exit leaves as [`RUnwind`]
429/// ([`call_unwinding`]).
430///
431/// # Safety
432///
433/// As [`call_unwinding`]; `expr` and `env` are rooted for the call, `env` an
434/// environment. The result is unprotected.
435pub(crate) unsafe fn eval_unwinding(expr: SEXP, env: SEXP) -> SEXP {
436    struct EvalData {
437        expr: SEXP,
438        env: SEXP,
439    }
440
441    unsafe extern "C-unwind" fn eval(data: *mut c_void) -> SEXP {
442        let data = unsafe { &*data.cast::<EvalData>() };
443        unsafe { sys::Rf_eval(data.expr, data.env) }
444    }
445
446    let mut data = EvalData { expr, env };
447    unsafe { call_unwinding(eval, (&raw mut data).cast()) }
448}
449
450/// Call `fun(data)` in its own `R_UnwindProtect`, in the caller's R context,
451/// and return its value.
452///
453/// An R exit from `fun` (an error, an exiting handler of the caller's
454/// `tryCatch()`, a restart, an interrupt) stops at that frame, never jumping
455/// over a Rust frame. Its cleanup turns the exit into a Rust unwind
456/// ([`RUnwind`]); the boundary resumes it.
457///
458/// # Safety
459///
460/// R's main thread, inside a miniextendr boundary (a `#[miniextendr]` body, a
461/// `with_r_unwind_protect*` closure, a guarded callback). `fun` holds nothing
462/// to drop across an R jump (it is called from C), and what `data` points to
463/// outlives the call. The result is unprotected.
464pub(crate) unsafe fn call_unwinding(
465    fun: unsafe extern "C-unwind" fn(*mut c_void) -> SEXP,
466    data: *mut c_void,
467) -> SEXP {
468    struct CallData {
469        fun: unsafe extern "C-unwind" fn(*mut c_void) -> SEXP,
470        data: *mut c_void,
471        token: SEXP,
472        boundary: u64,
473    }
474
475    unsafe extern "C-unwind" fn trampoline(data: *mut c_void) -> SEXP {
476        let data = unsafe { &*data.cast::<CallData>() };
477        // An R exit from here jumps to `R_UnwindProtect`, over no Rust frame
478        // that holds anything to drop.
479        unsafe { (data.fun)(data.data) }
480    }
481
482    unsafe extern "C-unwind" fn cleanup(data: *mut c_void, jump: Rboolean) {
483        if jump != Rboolean::FALSE {
484            let data = unsafe { &*data.cast::<CallData>() };
485            // A boundary the R code entered and an R jump left skipped its
486            // `leave`; this evaluation's boundary is the innermost again.
487            BOUNDARY.with(|current| current.set(data.boundary));
488            // R stopped its jump at our frame and would continue it after this
489            // returns. Carry it out as a Rust unwind instead. `resume_unwind`
490            // skips the panic hook: this is no panic.
491            std::panic::resume_unwind(Box::new(RUnwind {
492                token: data.token,
493                boundary: data.boundary,
494            }));
495        }
496    }
497
498    unsafe {
499        release_abandoned_off_main();
500        let token = sys::R_MakeUnwindCont();
501        sys::R_PreserveObject(token);
502        let boundary = BOUNDARY.with(Cell::get);
503        let mut data = CallData {
504            fun,
505            data,
506            token,
507            boundary,
508        };
509        let value = R_UnwindProtect_C_unwind(
510            Some(trampoline),
511            (&raw mut data).cast(),
512            Some(cleanup),
513            (&raw mut data).cast(),
514            token,
515        );
516        BOUNDARY.with(|current| current.set(boundary));
517        sys::R_ReleaseObject(token);
518        value
519    }
520}
521
522// endregion
523
524// region: Log drain integration
525
526/// Drain the cross-thread log queue if the `log` feature is enabled.
527///
528/// This is called at every exit point of `run_r_unwind_protect` (normal
529/// return, Rust panic, and immediately before `R_ContinueUnwind`) so that
530/// worker-thread log records always reach R's console before the FFI call
531/// returns or re-raises an R error.
532///
533/// When the `log` feature is disabled this compiles to a no-op; there is
534/// no runtime overhead.
535#[inline]
536fn drain_log_queue_if_available() {
537    #[cfg(feature = "log")]
538    crate::optionals::log_impl::drain_log_queue();
539}
540
541// endregion
542
543/// Core R_UnwindProtect wrapper. Returns `Ok(result)` on success,
544/// `Err(payload)` on Rust panic, or diverges via `R_ContinueUnwind` on R longjmp.
545///
546/// Handles: CallData boxing, trampoline, cleanup handler, continuation token,
547/// `Box::from_raw` reclamation on all non-diverging paths.
548///
549/// Drains the cross-thread log queue (when the `log` feature is enabled) at
550/// each exit point so worker-thread records reach R's console before the FFI
551/// boundary is crossed.
552fn run_r_unwind_protect<F, R>(f: F) -> Result<R, Box<dyn Any + Send>>
553where
554    F: FnOnce() -> R,
555{
556    /// Marker type for R errors caught by R_UnwindProtect's cleanup handler.
557    struct RErrorMarker;
558
559    struct CallData<F, R> {
560        f: Option<F>,
561        result: Option<R>,
562        panic_payload: Option<Box<dyn Any + Send>>,
563    }
564
565    unsafe extern "C-unwind" fn trampoline<F, R>(data: *mut c_void) -> SEXP
566    where
567        F: FnOnce() -> R,
568    {
569        if !!data.is_null() {
    {
        ::core::panicking::panic_fmt(format_args!("trampoline: data pointer is null"));
    }
};assert!(!data.is_null(), "trampoline: data pointer is null");
570        let data = unsafe { &mut *data.cast::<CallData<F, R>>() };
571        let f = data.f.take().expect("trampoline: closure already consumed");
572
573        match catch_unwind(AssertUnwindSafe(f)) {
574            Ok(result) => {
575                data.result = Some(result);
576                crate::SEXP::nil()
577            }
578            Err(payload) => {
579                data.panic_payload = Some(payload);
580                crate::SEXP::nil()
581            }
582        }
583    }
584
585    unsafe extern "C-unwind" fn cleanup_handler(_data: *mut c_void, jump: Rboolean) {
586        if jump != Rboolean::FALSE {
587            // R is about to longjmp - trigger a Rust panic so we can unwind properly
588            std::panic::panic_any(RErrorMarker);
589        }
590    }
591
592    let deferred_mark = crate::deferred_condition::mark();
593    let boundary = Boundary::enter();
594    unsafe {
595        let token = get_continuation_token();
596
597        let data = Box::into_raw(Box::new(CallData::<F, R> {
598            f: Some(f),
599            result: None,
600            panic_payload: None,
601        }));
602
603        let panic_result = catch_unwind(AssertUnwindSafe(|| {
604            R_UnwindProtect_C_unwind(
605                Some(trampoline::<F, R>),
606                data.cast(),
607                Some(cleanup_handler),
608                std::ptr::null_mut(),
609                token,
610            )
611        }));
612
613        let mut data = Box::from_raw(data);
614
615        match panic_result {
616            Ok(_) => {
617                // Check if trampoline caught a panic
618                if let Some(payload) = data.panic_payload.take() {
619                    drop(data);
620                    // Drain worker-thread log records before returning the panic
621                    // payload to the caller (which will convert it to an R error).
622                    drain_log_queue_if_available();
623                    // An R exit carried out of `call_unwinding` (#1835): the Rust
624                    // frames it crossed have dropped their values; continue it.
625                    let payload = resume_if_r_unwind(payload, deferred_mark, &boundary);
626                    Err(payload)
627                } else {
628                    // Normal completion - return the result
629                    let result = data
630                        .result
631                        .take()
632                        .expect("result not set after successful completion");
633                    drop(data);
634                    // Drain worker-thread log records on the normal success path.
635                    drain_log_queue_if_available();
636                    Ok(result)
637                }
638            }
639            Err(payload) => {
640                // Drop data first to run destructors
641                drop(data);
642                // Check if this was an R error or a Rust panic
643                if payload.downcast_ref::<RErrorMarker>().is_some() {
644                    // R error - drain log records before re-raising so worker
645                    // thread output is not lost even on error exits.
646                    drain_log_queue_if_available();
647                    // No signalling while resuming an R error or exiting handler.
648                    // Conditions from the abandoned call must not survive it.
649                    crate::deferred_condition::discard(deferred_mark);
650                    boundary.leave();
651                    // Continue R's unwind (diverges, never returns)
652                    R_ContinueUnwind(token);
653                } else {
654                    // Rust panic — drain before returning the payload.
655                    drain_log_queue_if_available();
656                    Err(payload)
657                }
658            }
659        }
660    }
661}
662
663/// Execute a closure with R unwind protection, raising any Rust panic as the R
664/// error a generated wrapper would raise for it.
665///
666/// If the closure panics, the panic is caught and converted to an R error
667/// (longjmp) with `rust_*` class layering. If R raises an error (longjmp), all
668/// Rust RAII resources are properly dropped before R continues unwinding.
669///
670/// **This is NOT the user-facing path for `#[miniextendr]` functions.** That
671/// path is [`with_r_unwind_protect`], which returns a tagged SEXP instead of
672/// longjmping (the macro-generated R wrapper raises the structured condition).
673///
674/// This raising-variant exists for guard sites that have no R wrapper between
675/// them and R's runtime:
676/// - ALTREP `RUnwind` guard callbacks (via the crate-private
677///   `with_r_unwind_protect_sourced`)
678/// - FFI guard tests / benchmarks exercising the raw `R_UnwindProtect` mechanism
679///
680/// In those contexts there is no consumer-side R wrapper to inspect a tagged
681/// SEXP. Panics are routed through `raise_rust_condition_via_stop` so they
682/// still receive `rust_*` class layering (issue #345), the same `kind` and
683/// the same fields as the wrapper's condition (#1768). Trait-ABI shims use a
684/// separate SEXP-returning variant ([`with_r_unwind_protect_shim`]) that
685/// re-panics at the View boundary.
686///
687/// # Arguments
688///
689/// * `f` - The closure to execute
690/// * `call` - Optional R call SEXP for better error messages
691///
692/// Deferred conditions are signalled before the outcome is returned or raised.
693/// Any R objects held by the generic return value must remain rooted during
694/// signalling. Return an [`crate::OwnedProtect`] for a freshly allocated SEXP,
695/// then call its `get()` after this guard returns. ALTREP's SEXP trampolines do
696/// this automatically. An R-origin error discards this call's queued conditions.
697pub fn with_r_unwind_protect_or_raise<F, R>(f: F, call: Option<SEXP>) -> R
698where
699    F: FnOnce() -> R,
700{
701    with_r_unwind_protect_sourced(f, call, crate::panic_telemetry::PanicSource::UnwindProtect)
702}
703
704/// Like [`with_r_unwind_protect_or_raise`], but reports panics with a custom
705/// `PanicSource`.
706///
707/// Used by `guarded_altrep_call` so that panics inside ALTREP callbacks with
708/// `AltrepGuard::RUnwind` are still attributed to `PanicSource::Altrep`.
709///
710/// Every outcome is raised through [`raise_rust_condition_via_stop`], which
711/// hands the tagged value [`with_r_unwind_protect`] would return to the R
712/// helper the generated wrappers call, so the condition is the wrapper's, with
713/// its `kind` (#1768). This works even in ALTREP callback context, where there
714/// is no R wrapper to inspect a tagged SEXP (Approach 3 from the issue-345
715/// plan):
716///
717/// - `RCondition::Error` (`error!()`) — `kind = "error"`, its classes ahead of
718///   the `rust_*` layering, its `data =` fields.
719///
720/// - `RCondition::Conversion` (`arg_error!`) — `kind = "conversion"`, with the
721///   crate-class marker replaced in Rust by the classes `miniextendr_init!`
722///   registered: no generated R is there to resolve it.
723///
724/// - A generic panic — `kind = "panic"`, with the `(at file:line)` suffix.
725///
726/// - `Warning`, `Message`, `Condition` — a plain R error with a diagnostic
727///   message and `kind = "panic"`. `warning!()`/`message!()` from ALTREP
728///   context cannot suspend execution for non-fatal signals; documented
729///   limitation.
730pub(crate) fn with_r_unwind_protect_sourced<F, R>(
731    f: F,
732    call: Option<SEXP>,
733    source: crate::panic_telemetry::PanicSource,
734) -> R
735where
736    F: FnOnce() -> R,
737{
738    let deferred_mark = crate::deferred_condition::mark();
739    let outcome = run_r_unwind_protect(f);
740    // Preserve the original panic site before condition handlers can re-enter
741    // Rust and overwrite the panic hook's thread-local location.
742    let panic_message = outcome.as_ref().err().and_then(|payload| {
743        (!payload.is::<crate::condition::RCondition>())
744            .then(|| panic_message_with_location(payload.as_ref()))
745    });
746    let queued = crate::deferred_condition::take_pending(deferred_mark);
747    let (outcome, panic_message) = if queued.is_empty() {
748        (outcome, panic_message)
749    } else {
750        // Move both the outcome and saved panic text INTO this guard: an
751        // exiting R handler must drop all their Rust resources too.
752        // SEXP-bearing outcomes must own their roots.
753        match run_r_unwind_protect(move || {
754            unsafe { crate::deferred_condition::signal_now(queued, call) };
755            (outcome, panic_message)
756        }) {
757            Ok(outcome) => outcome,
758            Err(payload) => (Err(payload), None),
759        }
760    };
761    match outcome {
762        Ok(result) => result,
763        Err(payload) => {
764            // region: RCondition recognition for the raising-variant path
765            if payload.is::<crate::condition::RCondition>() {
766                // Take ownership so the payload's parts move into the tagged
767                // value without cloning — same idiom as `with_r_unwind_protect_shim`.
768                let cond = *payload
769                    .downcast::<crate::condition::RCondition>()
770                    .expect("checked is::<RCondition> above");
771                let error_kind = #[allow(non_exhaustive_omitted_patterns)] match cond {
    crate::condition::RCondition::Error { .. } |
        crate::condition::RCondition::Conversion { .. } => true,
    _ => false,
}matches!(
772                    cond,
773                    crate::condition::RCondition::Error { .. }
774                        | crate::condition::RCondition::Conversion { .. }
775                );
776                // The kind is the one the tagged path writes for this payload.
777                let (kind, mut parts) = cond.into_parts();
778                if error_kind {
779                    // Approach 3 (issue-345): the helper the wrappers use raises
780                    // the value, so `tryCatch(rust_error = h, ...)`,
781                    // `tryCatch(my_class = h, ...)`, `e$kind` and the `data`
782                    // fields (issue #996 path 2) all match the wrapper's
783                    // condition. That copy of the helper cannot resolve an
784                    // `arg_error!`'s class marker (#1740): Rust puts the
785                    // package's registered classes in its place.
786                    parts.class = crate::condition::resolve_conversion_error_class_marker(
787                        parts.class,
788                        crate::condition::crate_conversion_error_class(),
789                    );
790                    crate::panic_telemetry::fire(&parts.message, source);
791                    unsafe {
792                        raise_rust_condition_via_stop(
793                            move || crate::error_value::condition_parts_value(kind, parts, call),
794                            call,
795                        )
796                    }
797                } else {
798                    // warning!/message!/condition! cannot be cleanly raised from an ALTREP
799                    // or connection callback (no mechanism to suspend execution for
800                    // non-fatal signals).
801                    // Documented degradation: a plain R error with a fixed diagnostic,
802                    // raised like the generic-panic branch below (`kind = "panic"`,
803                    // `rust_error` class layering, issue #366): an error reporting the
804                    // original kind would claim to be a warning. The classes and data
805                    // fields are dropped along with the message; the call choice is
806                    // kept (`call = none` drops the guard's call).
807                    let call = parts.call.apply(call);
808                    drop(parts);
809                    let msg = "warning!/message!/condition! from an ALTREP or connection \
810                               callback context cannot be raised as non-fatal signals; use \
811                               error!() instead. This context has no R wrapper to handle \
812                               signal restart.";
813                    crate::panic_telemetry::fire(msg, source);
814                    unsafe {
815                        raise_rust_condition_via_stop(|| panic_condition_value(msg, call), call)
816                    }
817                }
818            } else {
819                // Generic panic — no class layering, plain error string plus the
820                // `(at file:line)` suffix folded from the panic hook (this branch
821                // runs on the panicking thread for the ALTREP/FFI-guard path).
822                // Fire telemetry and raise via Approach 3 with rust_error class so
823                // tryCatch(rust_error = h, ...) matches even for plain panics.
824                let msg =
825                    panic_message.unwrap_or_else(|| panic_message_with_location(payload.as_ref()));
826                // The raise longjmps over this frame: free the payload first.
827                drop(payload);
828                crate::panic_telemetry::fire(&msg, source);
829                unsafe {
830                    raise_rust_condition_via_stop(move || panic_condition_value(&msg, call), call)
831                }
832            }
833            // endregion
834        }
835    }
836}
837
838/// Like [`with_r_unwind_protect`], but tailored for trait-ABI vtable shims.
839///
840/// Same tagged-SEXP behaviour as [`with_r_unwind_protect`], but intended for
841/// shim functions that have no R wrapper of their own. The tagged SEXP is
842/// returned to the View method wrapper, which calls
843/// [`crate::condition::repanic_if_rust_error`] to re-panic with the
844/// reconstructed [`enum@crate::condition::RCondition`]. The outer
845/// `with_r_unwind_protect` in the consumer's C entry point then catches the
846/// re-panic and builds the final tagged SEXP for the consumer's R wrapper.
847///
848/// R-origin errors (longjmp) still pass through via `R_ContinueUnwind` — the
849/// outer guard will catch them.
850///
851/// # PROTECT note
852///
853/// The returned SEXP is unprotected. The View method wrapper must not call any
854/// R API functions between receiving it and passing it to
855/// `repanic_if_rust_error`. `repanic_if_rust_error` reads the message/kind/class
856/// strings immediately and then panics (or returns), so the SEXP does not need
857/// protection beyond that window.
858pub fn with_r_unwind_protect_shim<F>(f: F) -> SEXP
859where
860    F: FnOnce() -> SEXP,
861{
862    // Conditions queued with `defer_warning` & co. inside the shim are signalled
863    // at this boundary: the consumer's queue is a different static (every
864    // package links its own copy of this crate), so nothing may be left behind.
865    let deferred_mark = crate::deferred_condition::mark();
866    let result = match run_r_unwind_protect(f) {
867        Ok(result) => result,
868        Err(payload) => {
869            // region: RCondition recognition — same as the tagged-SEXP path
870            if payload.is::<crate::condition::RCondition>() {
871                // Take ownership of the payload so the `data` Vec can be moved
872                // into `make_rust_condition_value` (consumed when materialised).
873                // A call-less payload writes the "no call" marker, which
874                // `from_tagged_sexp` reads back on the consumer's side.
875                let cond = *payload
876                    .downcast::<crate::condition::RCondition>()
877                    .expect("checked is::<RCondition> above");
878                // SAFETY: on the R main thread inside R_UnwindProtect.
879                unsafe { crate::error_value::rust_condition_value(cond, None) }
880            } else {
881                // Generic panic path — fold the hook-captured `(at file:line)` into
882                // the message (this shim runs on the panicking thread).
883                let msg = panic_message_with_location(payload.as_ref());
884                crate::panic_telemetry::fire(
885                    &msg,
886                    crate::panic_telemetry::PanicSource::UnwindProtect,
887                );
888                // SAFETY: on the R main thread inside R_UnwindProtect.
889                unsafe {
890                    crate::error_value::make_rust_condition_value(
891                        &msg,
892                        crate::error_value::kind::PANIC,
893                        None,
894                        None,
895                    )
896                }
897            }
898            // endregion
899        }
900    };
901    // SAFETY: vtable shims run on R's main thread, inside the consumer's `.Call`.
902    unsafe { crate::deferred_condition::finish(deferred_mark, result, None) }
903}
904
905/// Run a closure under `R_UnwindProtect`, returning a tagged condition SEXP on
906/// Rust panics instead of raising an R error.
907///
908/// This is **the** transport for all `#[miniextendr]` functions and methods.
909/// The returned error/condition SEXP is inspected by the generated R wrapper
910/// which raises a proper R condition past the Rust boundary, with `rust_*`
911/// class layering.
912///
913/// Recognises [`enum@crate::condition::RCondition`] payloads (from `error!()`,
914/// `warning!()`, `message!()`, `condition!()`) before falling through to the
915/// generic panic→string path.
916///
917/// R-origin errors (longjmp) still pass through via `R_ContinueUnwind`.
918///
919/// For guard sites that have no R wrapper to inspect a tagged SEXP (ALTREP
920/// `RUnwind` callbacks, FFI guard tests) see [`with_r_unwind_protect_or_raise`];
921/// for trait-ABI vtable shims see [`with_r_unwind_protect_shim`].
922pub fn with_r_unwind_protect<F>(f: F, call: Option<SEXP>) -> SEXP
923where
924    F: FnOnce() -> SEXP,
925{
926    match run_r_unwind_protect(f) {
927        Ok(result) => result,
928        Err(payload) => {
929            // region: RCondition recognition — must come before generic panic path
930            if payload.is::<crate::condition::RCondition>() {
931                // Take ownership so the `data` payload can be moved into
932                // `make_rust_condition_value` (consumed during materialisation).
933                let cond = *payload
934                    .downcast::<crate::condition::RCondition>()
935                    .expect("checked is::<RCondition> above");
936                // No panic telemetry for user-raised conditions — they are intentional.
937                // SAFETY: on the R main thread inside R_UnwindProtect.
938                return unsafe { crate::error_value::rust_condition_value(cond, call) };
939            }
940            // endregion
941
942            // Generic panic path — the primary user-facing route for
943            // `#[miniextendr]` fns running on the main thread. Fold the
944            // hook-captured `(at file:line)` into the message. (The RCondition
945            // branch above is deliberately untouched: error!/warning!/message!/
946            // condition! and Err/None carry no location suffix.)
947            let msg = panic_message_with_location(payload.as_ref());
948            crate::panic_telemetry::fire(&msg, crate::panic_telemetry::PanicSource::UnwindProtect);
949            // SAFETY: on the R main thread inside R_UnwindProtect.
950            unsafe {
951                crate::error_value::make_rust_condition_value(
952                    &msg,
953                    crate::error_value::kind::PANIC,
954                    None,
955                    call,
956                )
957            }
958        }
959    }
960}