vexide_panic/lib.rs
1//! Panic handler for [`vexide`](https://crates.io/crates/vexide).
2//!
3//! Supports capturing and printing backtraces to aid in debugging.
4//!
5//! If the `display_panics` feature is enabled, it will also display the panic message on the V5 Brain display.
6
7#![no_std]
8
9extern crate alloc;
10
11#[allow(unused_imports)]
12use alloc::{
13 boxed::Box,
14 string::{String, ToString},
15};
16use core::sync::atomic::{AtomicBool, Ordering};
17#[allow(unused_imports)]
18use core::{cell::UnsafeCell, fmt::Write};
19
20use vexide_core::{backtrace::Backtrace, println, sync::Mutex};
21#[cfg(feature = "display_panics")]
22use vexide_devices::{
23 display::{Display, Font, FontFamily, FontSize, Rect, Text},
24 math::Point2,
25};
26
27static FIRST_PANIC: AtomicBool = AtomicBool::new(true);
28
29/// Draw an error box to the display.
30///
31/// This function is internally used by the vexide panic handler for displaying
32/// panic messages graphically before exiting.
33#[cfg(feature = "display_panics")]
34fn draw_error(display: &mut Display, msg: &str, backtrace: &Backtrace) {
35 const ERROR_BOX_MARGIN: i16 = 16;
36 const ERROR_BOX_PADDING: i16 = 16;
37 const LINE_HEIGHT: i16 = 20;
38 const LINE_MAX_WIDTH: usize = 52;
39
40 fn draw_text(screen: &mut Display, buffer: &str, line: i16) {
41 screen.draw_text(
42 &Text::new(
43 buffer,
44 Font::new(FontSize::SMALL, FontFamily::Monospace),
45 Point2 {
46 x: ERROR_BOX_MARGIN + ERROR_BOX_PADDING,
47 y: ERROR_BOX_MARGIN + ERROR_BOX_PADDING + (line * LINE_HEIGHT),
48 },
49 ),
50 (255, 255, 255),
51 None,
52 );
53 }
54
55 display.set_render_mode(vexide_devices::display::RenderMode::Immediate);
56
57 let error_box_rect = Rect::new(
58 Point2 {
59 x: ERROR_BOX_MARGIN,
60 y: ERROR_BOX_MARGIN,
61 },
62 Point2 {
63 x: Display::HORIZONTAL_RESOLUTION - ERROR_BOX_MARGIN,
64 y: Display::VERTICAL_RESOLUTION - ERROR_BOX_MARGIN,
65 },
66 );
67
68 display.fill(&error_box_rect, (255, 0, 0));
69 display.stroke(&error_box_rect, (255, 255, 255));
70
71 let mut buffer = String::new();
72 let mut line: i16 = 0;
73
74 for (i, character) in msg.char_indices() {
75 if !character.is_ascii_control() {
76 buffer.push(character);
77 }
78
79 if character == '\n' || ((buffer.len() % LINE_MAX_WIDTH == 0) && (i > 0)) {
80 draw_text(display, &buffer, line);
81 line += 1;
82 buffer.clear();
83 }
84 }
85
86 if !buffer.is_empty() {
87 draw_text(display, &buffer, line);
88
89 line += 1;
90 }
91
92 line += 1;
93 draw_text(display, "stack backtrace:", line);
94 line += 1;
95
96 if !backtrace.frames.is_empty() {
97 const ROW_LENGTH: usize = 3;
98 for (col, frames) in backtrace.frames.chunks(ROW_LENGTH).enumerate() {
99 let mut msg = String::new();
100 for (row, frame) in frames.iter().enumerate() {
101 write!(msg, "{:>3}: {:?} ", col * ROW_LENGTH + row, frame).unwrap();
102 }
103 draw_text(display, msg.trim_end(), line);
104 line += 1;
105 }
106 }
107}
108
109/// The default panic handler.
110///
111/// This function is called when a panic occurs and no custom panic hook is set,
112/// but you can also use it as a fallback in your custom panic hook to print the
113/// panic message to the screen and stdout.
114///
115/// It will print the panic message to the serial connection, and if the
116/// `display_panics` feature is enabled, it will also display the panic message
117/// on the V5 Brain display.
118///
119/// Note that if `display_panics` is not enabled, this function will not return.
120/// It will immediately exit the program after printing the panic message. If
121/// you do not want this behavior, you should use your own
122///
123/// # Examples
124///
125/// ```
126/// # use vexide_panic::{default_panic_hook, set_hook};
127/// #
128/// set_hook(|info| {
129/// // Do something interesting with the panic info, like printing it to the
130/// // controller screen so the driver knows something has gone wrong.
131/// // ...
132///
133/// // Then, call the default panic hook to show the message on the screen
134/// default_panic_hook(info);
135/// });
136/// ```
137pub fn default_panic_hook(info: &core::panic::PanicInfo<'_>) {
138 println!("{info}");
139
140 let backtrace = Backtrace::capture();
141
142 #[cfg(feature = "display_panics")]
143 draw_error(
144 &mut unsafe { Display::new() },
145 &info.to_string(),
146 &backtrace,
147 );
148
149 if !backtrace.frames.is_empty() {
150 println!("{backtrace}");
151 }
152
153 #[cfg(not(feature = "display_panics"))]
154 vexide_core::program::exit();
155}
156
157/// The panic hook type
158///
159/// This mirrors the one available in the standard library.
160enum Hook {
161 Default,
162 Custom(Box<dyn Fn(&core::panic::PanicInfo<'_>) + Send>),
163}
164
165/// A word-for-word copy of the Rust `std` impl
166impl Hook {
167 #[inline]
168 fn into_box(self) -> Box<dyn Fn(&core::panic::PanicInfo<'_>) + Send> {
169 match self {
170 Hook::Default => Box::new(default_panic_hook),
171 Hook::Custom(hook) => hook,
172 }
173 }
174}
175
176static HOOK: Mutex<Hook> = Mutex::new(Hook::Default);
177
178/// Registers a custom panic hook, replacing the current one if any.
179///
180/// This can be used to, for example, output a different message to the screen
181/// than the default one shown when the `display_panics` feature is enabled, or
182/// to log the panic message to a log file or other output (you will need to use
183/// `unsafe` to get peripheral access).
184///
185/// **Note**: Just like in the standard library, a custom panic hook _does
186/// override_ the default panic hook. In `vexide`'s case, this means that the
187/// error _will not automatically print to the screen or console_ when you set
188/// a custom panic hook. You will need to either do that yourself in the custom
189/// panic hook, or call [`default_panic_hook`] from your hook.
190///
191/// # Examples
192///
193/// ```
194/// use vexide_panic::set_panic_hook;
195///
196/// set_hook(|info| {
197/// // Do something with the panic info
198/// // This is pretty useless since vexide already does this
199/// println!("{:?}", info);
200/// });
201/// ```
202pub fn set_hook<F>(hook: F)
203where
204 F: Fn(&core::panic::PanicInfo<'_>) + Send + 'static,
205{
206 // Try to lock the mutex. This should always succeed since the mutex is only
207 // locked when the program panics and by the set_hook and take_hook
208 // functions, which don't panic while holding a lock.
209 let mut guard = HOOK
210 .try_lock()
211 .expect("failed to set custom panic hook (mutex poisoned or locked)");
212 // If we used a simple assignment, like
213 // *guard = Hook::Custom(Box::new(hook));
214 // the old value will be dropped. Since the old value is `dyn`, it
215 // could have arbitrary side effects in its destructor, *including
216 // panicking*. We need to avoid panicking here, since the panic
217 // handler would not be able to lock the mutex, which is kind of
218 // important.
219 // Don't do anything that could panic until guard is dropped
220 let old_handler = core::mem::replace(&mut *guard, Hook::Custom(Box::new(hook))).into_box();
221 // Drop the guard first to avoid a deadlock
222 core::mem::drop(guard);
223 // Now we can drop the old handler
224 core::mem::drop(old_handler); // This could panic
225}
226
227/// Unregisters the current panic hook, if any, and returns it, replacing it
228/// with the default panic hook.
229///
230/// The default panic hook will remain registered if no custom hook was set.
231pub fn take_hook() -> Box<dyn Fn(&core::panic::PanicInfo<'_>) + Send> {
232 // Try to lock the mutex. This should always succeed since the mutex is only
233 // locked when the program panics and by the set_hook and take_hook
234 // functions, which don't panic while holding a lock.
235 let mut guard = HOOK
236 .try_lock()
237 .expect("failed to set custom panic hook (mutex locked)");
238 // Don't do anything that could panic until guard is dropped
239 let old_hook = core::mem::replace(&mut *guard, Hook::Default).into_box();
240 core::mem::drop(guard);
241 old_hook
242}
243
244/// The panic handler for vexide.
245#[panic_handler]
246pub fn panic(info: &core::panic::PanicInfo<'_>) -> ! {
247 // This can only occur if the panic handler itself has panicked (which can
248 // happen in hooks or if println!() fails), resulting in a potential stack
249 // overflow. In this instance, something is likely very wrong, so it's better
250 // to just abort rather than recursively panicking.
251 if !FIRST_PANIC.swap(false, Ordering::Relaxed) {
252 vexide_core::program::exit();
253 }
254
255 // Try to lock the HOOK mutex. If we can't, we'll just use the default panic
256 // handler, since it's probably not good to panic in the panic handler and
257 // leave the user clueless about what happened.
258 //
259 // We should be able to lock the mutex, since we know that the mutex is only
260 // otherwise locked in `take_hook` and `set_hook`, which don't panic.
261
262 // Allow if_let_rescope lint since we actually prefer the Rust 2024 rescope
263 // in this case.
264 // Formerly, in the `else` branch, the lock would be held to the end of the
265 // block, but it doesn't in the 2024 edition of Rust. That behavior is
266 // actually preferable here.
267 #[allow(if_let_rescope)]
268 if let Some(mut guard) = HOOK.try_lock() {
269 let hook = core::mem::replace(&mut *guard, Hook::Default);
270 // Drop the guard first to avoid preventing set_hook or take_hook from
271 // getting a hook
272 core::mem::drop(guard);
273 (hook.into_box())(info);
274 } else {
275 // Since this is in theory unreachable, if it is reached, let's ask the
276 // user to file a bug report.
277 // FIXME: use eprintln once armv7a-vex-v5 support in Rust is merged
278 println!("Panic handler hook mutex was locked, so the default panic hook will be used. This should never happen.");
279 println!("If you see this, please consider filing a bug: https://github.com/vexide/vexide/issues/new");
280 default_panic_hook(info);
281 }
282
283 // enter into an endless loop if the panic hook didn't exit the program
284 loop {
285 unsafe {
286 // Flush the serial buffer so that the panic message is printed
287 vex_sdk::vexTasksRun();
288 }
289 }
290}