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}