Skip to main content

asdf/
panic.rs

1//! Keeping Rust panics from crossing the C boundary.
2//!
3//! Unwinding out of an `extern "C"` function into C is undefined behaviour.
4//! Every entry point in this crate therefore runs its body inside
5//! [`guard`], which catches any panic, reports it once, and returns the
6//! caller-supplied fallback value instead.
7//!
8//! This is a backstop, not a design: a panic reaching here is a bug in the
9//! engine. It is reported to stderr on first occurrence so the bug is visible
10//! rather than silently swallowed.
11
12use core::panic::AssertUnwindSafe;
13use core::sync::atomic::{AtomicBool, Ordering};
14use std::panic::catch_unwind;
15
16static REPORTED: AtomicBool = AtomicBool::new(false);
17
18/// Run `body`, returning `fallback` if it panics.
19///
20/// The closure is treated as unwind-safe: state it touches lives behind
21/// handles the C caller owns, and a panic leaves that state untouched
22/// because the engine itself does not panic on error paths -- it returns
23/// `Result`.
24pub fn guard<T>(what: &'static str, fallback: T, body: impl FnOnce() -> T) -> T {
25    match catch_unwind(AssertUnwindSafe(body)) {
26        Ok(value) => value,
27        Err(payload) => {
28            report(what, &payload);
29            fallback
30        }
31    }
32}
33
34fn report(what: &'static str, payload: &Box<dyn core::any::Any + Send>) {
35    // Only the first panic is reported, so a caller looping over a broken
36    // file does not flood stderr.
37    if REPORTED.swap(true, Ordering::Relaxed) {
38        return;
39    }
40    let msg = payload
41        .downcast_ref::<&str>()
42        .map(|s| (*s).to_string())
43        .or_else(|| payload.downcast_ref::<String>().cloned())
44        .unwrap_or_else(|| "unknown panic".to_string());
45
46    eprintln!(
47        "libasdf-rs: internal error: a panic escaped {what}: {msg}\n\
48         libasdf-rs: this is a bug; the call returned a failure value instead.\n\
49         libasdf-rs: please report it at https://github.com/cruzzil/asdf/issues"
50    );
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn returns_the_value_when_nothing_panics() {
59        assert_eq!(guard("test", -1, || 42), 42);
60    }
61
62    #[test]
63    fn returns_the_fallback_on_panic() {
64        // Silence the default hook so the test output stays readable.
65        let prev = std::panic::take_hook();
66        std::panic::set_hook(Box::new(|_| {}));
67        let got = guard("test", -1, || panic!("boom"));
68        std::panic::set_hook(prev);
69        assert_eq!(got, -1);
70    }
71
72    #[test]
73    fn null_pointers_are_a_valid_fallback() {
74        let prev = std::panic::take_hook();
75        std::panic::set_hook(Box::new(|_| {}));
76        let got: *mut u8 = guard("test", core::ptr::null_mut(), || panic!("boom"));
77        std::panic::set_hook(prev);
78        assert!(got.is_null());
79    }
80}