Skip to main content

miniextendr_api/
backtrace.rs

1//! Configurable panic hook for miniextendr-based R packages.
2//!
3//! The hook is process-global (`std::panic::set_hook` writes to a process
4//! slot), but its closure lives in the DLL's code. If the package is
5//! unloaded (e.g. `library.dynam.unload` / `dyn.unload`) without removing
6//! the hook, the next panic anywhere in the process jumps to unmapped
7//! memory and tears down the SEH state on Windows — surfacing as "failed
8//! to initiate panic, error 5" in the next DLL that tries to unwind (#277).
9//!
10//! `miniextendr_panic_hook()` installs; `miniextendr_panic_hook_uninstall()`
11//! takes it back off. Both are idempotent and paired by the init / unload
12//! code in `worker.rs`.
13
14use std::cell::Cell;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17/// True iff this DLL instance has installed the panic hook.
18///
19/// Per-DLL: each dyn.load of the compiled artifact gets a fresh static, so
20/// the install/uninstall lifecycle is scoped to one load.
21static INSTALLED: AtomicBool = AtomicBool::new(false);
22
23thread_local! {
24    /// Source location of the most recent panic on *this* thread, captured by
25    /// the panic hook. `Location` borrows the `PanicHookInfo`, so we snapshot an
26    /// owned `(file, line)`.
27    ///
28    /// Per-thread because the hook fires on the panicking thread: a worker-thread
29    /// panic records here on the worker; a main-thread panic records here on main.
30    /// [`take_last_panic_location`] reads-and-clears so a stale location can never
31    /// leak onto a later, location-less message on the same (reused) thread.
32    static LAST_PANIC_LOCATION: Cell<Option<(String, u32)>> = const { Cell::new(None) };
33}
34
35/// Record a panic's source location into the current thread's take-once slot.
36///
37/// Called unconditionally from the hook (before the `MINIEXTENDR_BACKTRACE`
38/// env check) so the location is available to the panic-stringification sites
39/// regardless of whether the stderr traceback is enabled.
40fn record_panic_location(info: &std::panic::PanicHookInfo<'_>) {
41    let loc = info.location().map(|l| (l.file().to_string(), l.line()));
42    LAST_PANIC_LOCATION.with(|cell| cell.set(loc));
43}
44
45/// Take (read + clear) the last panic location recorded on the current thread.
46///
47/// Returns `None` when no panic hook fired on this thread since the last take
48/// (e.g. the hook was never installed, as in some unit tests / the engine).
49/// Clearing on read means a location from a panic that was caught-and-diverted
50/// (e.g. the internal `RErrorMarker` on the R-longjmp path) never bleeds onto an
51/// unrelated later message.
52pub(crate) fn take_last_panic_location() -> Option<(String, u32)> {
53    LAST_PANIC_LOCATION.with(|cell| cell.take())
54}
55
56/// Register the miniextendr panic hook.
57///
58/// If `MINIEXTENDR_BACKTRACE` is truthy (`yes`/`true`/`1`/`on`, per
59/// `crate::env_flag::parse_bool`), the default Rust panic hook runs (full
60/// traceback printed to stderr); otherwise the hook swallows the panic output
61/// silently so the R error (emitted by `panic_message_to_r_error`) is what
62/// users see. Unrecognized values default to off.
63///
64/// Idempotent within a DLL instance: the first call installs, subsequent
65/// calls are no-ops. If the DLL is unloaded and loaded again, the new
66/// instance has its own `INSTALLED` flag and installs afresh.
67#[unsafe(no_mangle)]
68pub extern "C-unwind" fn miniextendr_panic_hook() {
69    if INSTALLED
70        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
71        .is_err()
72    {
73        return;
74    }
75
76    let default_hook = std::panic::take_hook();
77    std::panic::set_hook(Box::new(move |x| {
78        // Capture the panic location for the R-facing message BEFORE the env
79        // check, so `(at file:line)` is surfaced whether or not the stderr
80        // traceback is enabled.
81        record_panic_location(x);
82        let show_traceback = std::env::var("MINIEXTENDR_BACKTRACE")
83            .ok()
84            .and_then(|v| crate::env_flag::parse_bool(&v))
85            .unwrap_or(false);
86        if show_traceback {
87            default_hook(x)
88        }
89    }));
90}
91
92/// Remove the miniextendr panic hook and revert to Rust's default.
93///
94/// Called from `miniextendr_runtime_shutdown` (which runs in
95/// `R_unload_<pkg>`). Must run before the DLL's code pages are unmapped —
96/// otherwise the next panic, anywhere in the process, executes freed
97/// memory. See #277.
98///
99/// Idempotent: safe to call even if the hook wasn't installed.
100pub(crate) fn miniextendr_panic_hook_uninstall() {
101    if !INSTALLED.swap(false, Ordering::AcqRel) {
102        return;
103    }
104    // Take and drop our hook. `take_hook` returns the current hook and
105    // resets the process slot to Rust's default hook. Dropping our
106    // `Box<dyn Fn>` also drops the captured `default_hook`, which is fine:
107    // we're intentionally reverting to the process default, not to
108    // whatever hook existed before install.
109    let _our_hook = std::panic::take_hook();
110}