use std::any::Any;
use std::panic::AssertUnwindSafe;
pub fn catch_user_panic<F: FnOnce()>(site: &str, f: F) {
let _ = catch_user_panic_result(site, f);
}
#[must_use = "return an ABI-safe fallback when the callback panics"]
pub fn catch_user_panic_result<R, F: FnOnce() -> R>(site: &str, f: F) -> Option<R> {
let boundary_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
match std::panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(result) => Some(result),
Err(payload) => {
log_callback_panic(site, payload.as_ref());
drop(payload);
None
}
}
}));
match boundary_result {
Ok(result) => result,
Err(payload) => {
log_callback_panic(site, payload.as_ref());
drop_payload_best_effort(payload);
None
}
}
}
#[must_use = "return an ABI-safe fallback when any protected phase panics"]
pub fn catch_user_panic_result_with_cleanup<S, R, F, C>(
site: &str,
mut state: S,
mut f: F,
mut cleanup: C,
) -> Option<R>
where
F: FnMut(&mut S) -> R,
C: FnMut(&mut S),
{
let callback_result = catch_user_panic_result(site, || f(&mut state));
let cleanup_succeeded = catch_user_panic_result(site, || cleanup(&mut state)).is_some();
let callback_drop_succeeded = catch_user_panic_result(site, || drop(f)).is_some();
let cleanup_drop_succeeded = catch_user_panic_result(site, || drop(cleanup)).is_some();
let state_drop_succeeded = catch_user_panic_result(site, || drop(state)).is_some();
if callback_drop_succeeded
&& cleanup_succeeded
&& cleanup_drop_succeeded
&& state_drop_succeeded
{
callback_result
} else {
if let Some(result) = callback_result {
catch_user_panic(site, || drop(result));
}
None
}
}
pub fn log_callback_panic(site: &str, payload: &(dyn Any + Send)) {
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let message = payload.downcast_ref::<&'static str>().map_or_else(
|| {
payload
.downcast_ref::<String>()
.map_or("<non-string panic payload>", String::as_str)
},
|message| *message,
);
eprintln!("doom-fish-utils: panic in {site} caught at C ABI boundary: {message}");
}));
if let Err(payload) = result {
drop_payload_best_effort(payload);
}
}
fn drop_payload_best_effort(payload: Box<dyn Any + Send>) {
if let Err(undroppable_payload) = std::panic::catch_unwind(AssertUnwindSafe(|| drop(payload))) {
std::mem::forget(undroppable_payload);
}
}
#[cfg(test)]
mod tests {
use std::panic::{catch_unwind, panic_any, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use super::{catch_user_panic, catch_user_panic_result, catch_user_panic_result_with_cleanup};
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
#[test]
fn normal_panic_payload_is_dropped() {
let dropped = Arc::new(AtomicBool::new(false));
let payload = DropFlag(Arc::clone(&dropped));
catch_user_panic("normal_panic_payload_is_dropped", move || {
panic_any(payload);
});
assert!(dropped.load(Ordering::SeqCst));
}
#[test]
fn result_helper_preserves_success_and_maps_panic_to_none() {
assert_eq!(catch_user_panic_result("result success", || 42), Some(42));
assert_eq!(
catch_user_panic_result("result panic", || -> u32 {
panic!("result callback panic");
}),
None
);
}
#[test]
fn cleanup_helper_preserves_successful_result() {
let cleanup_ran = Arc::new(AtomicBool::new(false));
let cleanup_ran_in_closure = Arc::clone(&cleanup_ran);
let result = catch_user_panic_result_with_cleanup(
"cleanup_helper_preserves_successful_result",
40_u32,
|state| *state + 2,
move |_state| {
cleanup_ran_in_closure.store(true, Ordering::SeqCst);
},
);
assert_eq!(result, Some(42));
assert!(cleanup_ran.load(Ordering::SeqCst));
}
#[test]
fn captured_release_runs_during_callback_unwind() {
let released = Arc::new(AtomicBool::new(false));
let guard = DropFlag(Arc::clone(&released));
catch_user_panic("captured_release_runs_during_callback_unwind", move || {
let _guard = guard;
panic!("callback panic");
});
assert!(released.load(Ordering::SeqCst));
}
struct PanicOnDrop;
impl Drop for PanicOnDrop {
fn drop(&mut self) {
panic!("panic while dropping panic payload");
}
}
#[test]
fn single_panic_payload_destructor_is_contained() {
let result = catch_unwind(AssertUnwindSafe(|| {
catch_user_panic("single_panic_payload_destructor_is_contained", || {
panic_any(PanicOnDrop);
});
}));
assert!(result.is_ok());
}
struct ReleaseGuard {
released: Arc<AtomicBool>,
}
impl Drop for ReleaseGuard {
fn drop(&mut self) {
self.released.store(true, Ordering::SeqCst);
panic!("panic in captured release");
}
}
#[test]
fn single_capture_drop_panic_after_normal_return_is_contained() {
let released = Arc::new(AtomicBool::new(false));
let guard = ReleaseGuard {
released: Arc::clone(&released),
};
let result = catch_unwind(AssertUnwindSafe(|| {
catch_user_panic(
"single_capture_drop_panic_after_normal_return_is_contained",
move || {
let _guard = guard;
},
);
}));
assert!(result.is_ok());
assert!(released.load(Ordering::SeqCst));
}
struct OrderedPanicOnDrop {
sequence: Arc<AtomicUsize>,
drop_order: Arc<AtomicUsize>,
}
impl Drop for OrderedPanicOnDrop {
fn drop(&mut self) {
let order = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
self.drop_order.store(order, Ordering::SeqCst);
panic!("single callback closure drop panic");
}
}
#[test]
fn cleanup_runs_before_single_callback_closure_drop_panic() {
let sequence = Arc::new(AtomicUsize::new(0));
let cleanup_order = Arc::new(AtomicUsize::new(0));
let drop_order = Arc::new(AtomicUsize::new(0));
let guard = OrderedPanicOnDrop {
sequence: Arc::clone(&sequence),
drop_order: Arc::clone(&drop_order),
};
let cleanup_sequence = Arc::clone(&sequence);
let cleanup_order_in_closure = Arc::clone(&cleanup_order);
let survived = catch_unwind(AssertUnwindSafe(|| {
let result = catch_user_panic_result_with_cleanup(
"cleanup_runs_before_single_callback_closure_drop_panic",
(),
move |_state| -> u32 {
let _guard = &guard;
panic!("callback panic");
},
move |_state| {
let order = cleanup_sequence.fetch_add(1, Ordering::SeqCst) + 1;
cleanup_order_in_closure.store(order, Ordering::SeqCst);
},
);
assert_eq!(result, None);
}));
assert!(survived.is_ok());
assert_eq!(cleanup_order.load(Ordering::SeqCst), 1);
assert_eq!(drop_order.load(Ordering::SeqCst), 2);
}
}