Backtrace: std and threadsafe bevy_error_panic_hook (#18235)

# Objective

Make `bevy_error_panic_hook` threadsafe. As it relies on a global
variable, it fails when multiple threads panic.

## Solution

Switch from a global variable for storing whether an error message was
printed to a thread-local one.

`thread_local` is in `std`; the `backtrace` already relies on `std`
APIs. It didn't depend on the `std` feature though, so I've added that.
I've also put `bevy_error_panic_hook` behind the `backtrace` feature,
since it relies on the thread local variable, which fixes #18231.

## Testing

The following now loops instead of crashing:

```rust
std:🧵:scope(|s| {
    use bevy_ecs::error::*;

    #[derive(Debug)]
    struct E;
    impl std::fmt::Display for E {
        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            todo!()
        }
    }
    impl std::error::Error for E {}

    std::panic::set_hook(Box::new(bevy_error_panic_hook(|_| {
        unreachable!();
    })));
    for _ in 0..2 {
        s.spawn(|| {
            loop {
                let _ = std::panic::catch_unwind(|| {
                    panic!("{:?}", BevyError::from(E));
                });
            }
        });
    }
});
```
This commit is contained in:
SpecificProtagonist 2025-03-10 22:16:14 +01:00 committed by GitHub
parent b054d116a6
commit cdef139710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 8 additions and 7 deletions

View File

@ -37,7 +37,7 @@ reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
configurable_error_handler = []
## Enables automatic backtrace capturing in BevyError
backtrace = []
backtrace = ["std"]
# Debugging Features

View File

@ -117,7 +117,7 @@ impl Debug for BevyError {
}
if !full_backtrace {
if std::thread::panicking() {
SKIP_NORMAL_BACKTRACE.store(1, core::sync::atomic::Ordering::Relaxed);
SKIP_NORMAL_BACKTRACE.set(true);
}
writeln!(f, "{FILTER_MESSAGE}")?;
}
@ -132,22 +132,23 @@ impl Debug for BevyError {
const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
#[cfg(feature = "backtrace")]
static SKIP_NORMAL_BACKTRACE: core::sync::atomic::AtomicUsize =
core::sync::atomic::AtomicUsize::new(0);
std::thread_local! {
static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
const { core::cell::Cell::new(false) };
}
/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.
#[cfg(feature = "std")]
#[cfg(feature = "backtrace")]
pub fn bevy_error_panic_hook(
current_hook: impl Fn(&std::panic::PanicHookInfo),
) -> impl Fn(&std::panic::PanicHookInfo) {
move |info| {
if SKIP_NORMAL_BACKTRACE.load(core::sync::atomic::Ordering::Relaxed) > 0 {
if SKIP_NORMAL_BACKTRACE.replace(false) {
if let Some(payload) = info.payload().downcast_ref::<&str>() {
std::println!("{payload}");
} else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {
std::println!("{payload}");
}
SKIP_NORMAL_BACKTRACE.store(0, core::sync::atomic::Ordering::Relaxed);
return;
}