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::sync::atomic::{AtomicBool, Ordering};
15
16/// True iff this DLL instance has installed the panic hook.
17///
18/// Per-DLL: each dyn.load of the compiled artifact gets a fresh static, so
19/// the install/uninstall lifecycle is scoped to one load.
20static INSTALLED: AtomicBool = AtomicBool::new(false);
21
22/// Register the miniextendr panic hook.
23///
24/// If `MINIEXTENDR_BACKTRACE` is set to `true` or `1`, the default Rust
25/// panic hook runs (full traceback printed to stderr); otherwise the hook
26/// swallows the panic output silently so the R error (emitted by
27/// `panic_message_to_r_error`) is what users see.
28///
29/// Idempotent within a DLL instance: the first call installs, subsequent
30/// calls are no-ops. If the DLL is unloaded and loaded again, the new
31/// instance has its own `INSTALLED` flag and installs afresh.
32#[unsafe(no_mangle)]
33pub extern "C-unwind" fn miniextendr_panic_hook() {
34    if INSTALLED
35        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
36        .is_err()
37    {
38        return;
39    }
40
41    let default_hook = std::panic::take_hook();
42    std::panic::set_hook(Box::new(move |x| {
43        let show_traceback = std::env::var("MINIEXTENDR_BACKTRACE")
44            .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
45            .unwrap_or(false);
46        if show_traceback {
47            default_hook(x)
48        }
49    }));
50}
51
52/// Remove the miniextendr panic hook and revert to Rust's default.
53///
54/// Called from `miniextendr_runtime_shutdown` (which runs in
55/// `R_unload_<pkg>`). Must run before the DLL's code pages are unmapped —
56/// otherwise the next panic, anywhere in the process, executes freed
57/// memory. See #277.
58///
59/// Idempotent: safe to call even if the hook wasn't installed.
60pub(crate) fn miniextendr_panic_hook_uninstall() {
61    if !INSTALLED.swap(false, Ordering::AcqRel) {
62        return;
63    }
64    // Take and drop our hook. `take_hook` returns the current hook and
65    // resets the process slot to Rust's default hook. Dropping our
66    // `Box<dyn Fn>` also drops the captured `default_hook`, which is fine:
67    // we're intentionally reverting to the process default, not to
68    // whatever hook existed before install.
69    let _our_hook = std::panic::take_hook();
70}