Skip to main content

doom_fish_utils/
panic_safe.rs

1//! Panic-safety helpers for the C ABI boundary.
2//!
3//! Rust panics that unwind across `extern "C"` into Swift are undefined
4//! behaviour. Every extern callback in this crate that invokes user code
5//! must wrap that invocation in [`catch_user_panic`], which catches the
6//! panic, logs a best-effort diagnostic to stderr, and never returns a
7//! panic to its caller.
8//!
9//! This is intentionally a single shared helper rather than ad-hoc
10//! `catch_unwind` calls so the diagnostic format and the
11//! "stderr-write itself might panic" defence-in-depth stay consistent.
12
13use std::any::Any;
14use std::panic::AssertUnwindSafe;
15
16/// Run `f` and swallow any panic it produces.
17///
18/// On panic, writes a best-effort diagnostic to stderr identifying the
19/// callback site and the panic message (when the payload is a `&str` or
20/// `String`). The diagnostic write is itself wrapped in `catch_unwind`
21/// so an allocator failure or broken stderr can never propagate out.
22///
23/// `AssertUnwindSafe` is required because trait objects are not
24/// generally `UnwindSafe` and we accept the user's responsibility for
25/// their own state consistency on panic.
26pub fn catch_user_panic<F: FnOnce()>(site: &str, f: F) {
27    if let Err(payload) = std::panic::catch_unwind(AssertUnwindSafe(f)) {
28        log_callback_panic(site, payload.as_ref());
29    }
30}
31
32/// Best-effort logger for panics caught at the C ABI boundary.
33///
34/// Public to support call sites that already have a panic payload
35/// (e.g. those that need to dispatch multiple callbacks individually).
36/// Most callers want [`catch_user_panic`] instead.
37pub fn log_callback_panic(site: &str, payload: &(dyn Any + Send)) {
38    let message = payload.downcast_ref::<&'static str>().map_or_else(
39        || {
40            payload
41                .downcast_ref::<String>()
42                .cloned()
43                .unwrap_or_else(|| "<non-string panic payload>".to_string())
44        },
45        |s| (*s).to_string(),
46    );
47    let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
48        eprintln!("doom-fish-utils: panic in {site} caught at C ABI boundary: {message}");
49    }));
50}