From 5a07a635486c3a072e6578fbb725da1bab9a774c Mon Sep 17 00:00:00 2001 From: HampusM Date: Mon, 6 Jul 2026 21:57:52 +0200 Subject: feat(engine): make context addable to errors & results --- Cargo.lock | 57 ++--------- engine-ecs/Cargo.toml | 8 +- engine-ecs/src/error.rs | 266 +++++++++++++++++++----------------------------- engine-ecs/src/lib.rs | 2 + engine/src/windowing.rs | 6 +- 5 files changed, 127 insertions(+), 212 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55e56fd..ed1e171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.0" @@ -117,21 +108,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "base64" version = "0.22.1" @@ -626,11 +602,11 @@ name = "engine-ecs" version = "0.1.0" dependencies = [ "anyhow", - "backtrace", "criterion", "engine-ecs-macros", "engine-reflection", "hashbrown 0.15.2", + "libc", "parking_lot", "paste", "seq-macro", @@ -639,6 +615,7 @@ dependencies = [ "tracing-subscriber", "util-macros", "vizoxide", + "windows-sys 0.61.2", ] [[package]] @@ -852,12 +829,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gl_generator" version = "0.14.0" @@ -1569,15 +1540,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -1888,12 +1850,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2831,6 +2787,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/engine-ecs/Cargo.toml b/engine-ecs/Cargo.toml index 0c9da60..001cf54 100644 --- a/engine-ecs/Cargo.toml +++ b/engine-ecs/Cargo.toml @@ -14,12 +14,18 @@ tracing = "0.1.39" hashbrown = "0.15.2" parking_lot = "0.12.3" anyhow = "1.0.102" -backtrace = "0.3.76" engine-ecs-macros = { workspace = true } engine-reflection = { workspace = true } util-macros = { workspace = true } vizoxide = { version = "1.0.5", optional = true } +[target.'cfg(windows)'.dependencies.windows-sys] +version = "0.61.2" +features = ["Win32_System_Threading"] + +[target.'cfg(target_os = "linux")'.dependencies.libc] +version = "0.2.186" + [dev-dependencies.criterion] version = "0.5.1" default-features = false diff --git a/engine-ecs/src/error.rs b/engine-ecs/src/error.rs index c3da052..2654dde 100644 --- a/engine-ecs/src/error.rs +++ b/engine-ecs/src/error.rs @@ -1,17 +1,15 @@ -use std::fmt::{Debug, Display, Write as _}; - -use backtrace::Backtrace; +use std::fmt::{Debug, Display}; use crate::World; #[macro_export] macro_rules! error { ($lit: literal) => { - $crate::error::Error::from($lit) + $crate::error::Error::message($lit) }; ($lit: literal, $($tt: tt)*) => { - $crate::error::Error::from(std::format!($lit, $($tt)*)) + $crate::error::Error::message(std::format!($lit, $($tt)*)) }; ($err: expr) => { @@ -21,24 +19,30 @@ macro_rules! error { pub struct Error { - inner: Box, - backtrace: Backtrace, + inner: anyhow::Error, } impl Error { - pub fn resolve_backtrace(&mut self) + pub fn new(err: Err) -> Self + where + Err: std::error::Error + Send + Sync + 'static, { - self.backtrace.resolve(); + Self::from(err) } - fn is_backtrace_resolved(&self) -> bool + pub fn message(message: Message) -> Self + where + Message: Display + Debug + Send + Sync + 'static, { - let Some(first_frame) = self.backtrace.frames().first() else { - return false; - }; + Self { inner: anyhow::Error::msg(message) } + } - !first_frame.symbols().is_empty() + pub fn context(self, context: Ctx) -> Error + where + Ctx: Display + Send + Sync + 'static, + { + Self { inner: self.inner.context(context) } } } @@ -46,59 +50,7 @@ impl Debug for Error { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error = &*self.inner; - - write!(formatter, "{error}")?; - - if let Some(cause) = error.source() { - write!(formatter, "\n\nCaused by:")?; - let multiple = cause.source().is_some(); - for (n, error) in anyhow::Chain::new(cause).enumerate() { - writeln!(formatter)?; - - let mut indented = Indented { - inner: formatter, - number: if multiple { Some(n) } else { None }, - started: false, - }; - write!(indented, "{error}")?; - } - } - - if std::env::var_os("ENGINE_ECS_BACKTRACE") - .is_none_or(|backtrace_enabled| backtrace_enabled != "1") - { - write!( - formatter, - concat!( - "\n\nnote: run with `ENGINE_ECS_BACKTRACE=1` environment variable ", - "to display a engine backtrace" - ) - )?; - - return Ok(()); - } - - let mut cloned_backtrace; - - let backtrace = if self.is_backtrace_resolved() { - &self.backtrace - } else { - cloned_backtrace = self.backtrace.clone(); - cloned_backtrace.resolve(); - &cloned_backtrace - }; - - write!( - formatter, - "\n\nStack backtrace:\n{:?}", - std::fmt::from_fn(|backtrace_formatter| fmt_backtrace( - backtrace, - backtrace_formatter - )) - )?; - - Ok(()) + ::fmt(&self.inner, formatter) } } @@ -106,31 +58,64 @@ impl Display for Error { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error = &*self.inner; + ::fmt(&self.inner, formatter) + } +} - write!(formatter, "{error}")?; +impl From for Error +where + Err: std::error::Error + Send + Sync + 'static, +{ + fn from(err: Err) -> Self + { + Self { inner: err.into() } + } +} - if formatter.alternate() { - let chain = anyhow::Chain::new(error); - for cause in chain.skip(1) { - write!(formatter, ": {}", cause)?; - } - } +pub trait Context +{ + fn context(self, context: Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static; - Ok(()) - } + fn with_context(self, context_func: impl FnOnce() -> Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static; } -impl From for Error +impl Context for Result where - Box: From, + Err: std::error::Error + Send + Sync + 'static, { - fn from(err: Err) -> Self + fn context(self, context: Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static, { - Self { - inner: err.into(), - backtrace: Backtrace::new_unresolved(), - } + self.map_err(|err| Error::from(err).context(context)) + } + + fn with_context(self, context_func: impl FnOnce() -> Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static, + { + self.map_err(|err| Error::from(err).context(context_func())) + } +} + +impl Context for Result +{ + fn context(self, context: Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static, + { + self.map_err(|err| err.context(context)) + } + + fn with_context(self, context_func: impl FnOnce() -> Ctx) -> Result + where + Ctx: Display + Send + Sync + 'static, + { + self.map_err(|err| err.context(context_func())) } } @@ -164,14 +149,12 @@ impl Display for SourceKind } } -pub fn err_handler_panic(_world: &World, mut err: Error, err_metadata: Metadata) +pub fn err_handler_panic(_world: &World, err: Error, err_metadata: Metadata) { - err.resolve_backtrace(); - - panic!( - "Error occurred in {} '{}': {err:?}", + std::panic::panic_any(err.context(format!( + "Error occurred in {} '{}'", err_metadata.source_kind, err_metadata.source_name - ); + ))); } pub fn err_handler_log_error(_world: &World, err: Error, err_metadata: Metadata) @@ -183,90 +166,49 @@ pub fn err_handler_log_error(_world: &World, err: Error, err_metadata: Metadata) ); } -fn fmt_backtrace( - backtrace: &Backtrace, - fmt: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result +pub(crate) fn set_panic_hook() { - let style = if fmt.alternate() { - backtrace::PrintFmt::Full - } else { - backtrace::PrintFmt::Short - }; + let previous_panic_hook = std::panic::take_hook(); - // When printing paths we try to strip the cwd if it exists, otherwise - // we just print the path as-is. Note that we also only do this for the - // short format, because if it's full we presumably want to print - // everything. - let cwd = std::env::current_dir(); - let mut print_path = - move |fmt: &mut std::fmt::Formatter<'_>, - path: backtrace::BytesOrWideString<'_>| { - let path = path.into_path_buf(); - if style != backtrace::PrintFmt::Full { - if let Ok(cwd) = &cwd { - if let Ok(suffix) = path.strip_prefix(cwd) { - return std::fmt::Display::fmt(&suffix.display(), fmt); - } - } - } - std::fmt::Display::fmt(&path.display(), fmt) + std::panic::set_hook(Box::new(move |info| { + let Some(err) = info.payload().downcast_ref::() else { + return previous_panic_hook(info); }; - let mut f = backtrace::BacktraceFmt::new(fmt, style, &mut print_path); + print!( + "Thread '{}' ({:?}) panicked", + std::thread::current().name().unwrap_or(""), + get_current_os_thread_id() + ); - f.add_context()?; + if let Some(location) = info.location() { + print!(" at {location}"); + } - for frame in backtrace.frames() { - if frame.symbols().iter().all(|symbol| { - symbol.name().is_some_and(|symbol_name| { - let symbol_name = symbol_name.to_string(); + print!("\n{err:?}"); - symbol_name - .contains(">::from") - }) - }) { - continue; + if err.inner.backtrace().status() == std::backtrace::BacktraceStatus::Disabled { + print!(concat!( + "\nnote: run with `RUST_BACKTRACE=1` environment variable ", + "to display a backtrace" + )); } - f.frame().backtrace_frame(frame)?; - } - f.finish()?; - Ok(()) + println!(""); + })); } -struct Indented<'a, D> +fn get_current_os_thread_id() -> u64 { - inner: &'a mut D, - number: Option, - started: bool, -} - -impl std::fmt::Write for Indented<'_, T> -where - T: std::fmt::Write, -{ - fn write_str(&mut self, s: &str) -> std::fmt::Result - { - for (i, line) in s.split('\n').enumerate() { - if !self.started { - self.started = true; - match self.number { - Some(number) => write!(self.inner, "{: >5}: ", number)?, - None => self.inner.write_str(" ")?, - } - } else if i > 0 { - self.inner.write_char('\n')?; - if self.number.is_some() { - self.inner.write_str(" ")?; - } else { - self.inner.write_str(" ")?; - } - } - - self.inner.write_str(line)?; + cfg_select! { + target_os = "linux" => { + (unsafe { libc::gettid() }) as u64 + } + windows => { + (unsafe { windows_sys::Win32::System::Threading::GetCurrentThreadId() }) as u64 + } + _ => { + compile_error!("Unsupported target OS"); } - - Ok(()) } } diff --git a/engine-ecs/src/lib.rs b/engine-ecs/src/lib.rs index a426b0b..c502b06 100644 --- a/engine-ecs/src/lib.rs +++ b/engine-ecs/src/lib.rs @@ -110,6 +110,8 @@ impl World world.add_sole(Stats::default()).ok(); + crate::error::set_panic_hook(); + world } diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs index 6d14af1..6086df8 100644 --- a/engine/src/windowing.rs +++ b/engine/src/windowing.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, Condvar, Mutex, Weak}; use std::thread::Builder as ThreadBuilder; use crossbeam_queue::ArrayQueue; -use ecs::error::Error as EcsError; use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle}; use winit::application::ApplicationHandler; use winit::error::EventLoopError; @@ -48,6 +47,7 @@ use crate::windowing::window::{ Id as WindowId, Window, }; +use crate::{error, Error as EngineError}; pub mod dpi; pub mod keyboard; @@ -133,7 +133,7 @@ fn update_stuff( mut mouse_buttons: Single, mut actions: Actions, entity_obtainer: EntityObtainer, -) -> Result<(), EcsError> +) -> Result<(), EngineError> { if context.display_handle.is_none() { cold_path(); @@ -267,7 +267,7 @@ fn update_stuff( if shared_state.thread_panicked.load(Ordering::Relaxed) { cold_path(); - return Err("Windowing app thread panicked".into()); + return Err(error!("Windowing app thread panicked")); } if shared_state.has_thread_err.load(Ordering::Relaxed) { -- cgit v1.2.3-18-g5258