Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error applet support + error applet panic hook #162

Merged
merged 17 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions ctru-rs/src/applets/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Error applet
//!
//! This applet displays error text as a pop-up message on the lower screen.
FenrirWolf marked this conversation as resolved.
Show resolved Hide resolved
use crate::services::{apt::Apt, gfx::Gfx};

use ctru_sys::errorConf;

/// Configuration struct to set up the Error applet.
#[doc(alias = "errorConf")]
pub struct PopUp {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I kind of liked the ErrorApplet name from before, but this works I guess. If not the original name, maybe it could be clearer that it's a pop up message. PopUpMessage?

state: Box<errorConf>,
}

/// The kind of error applet to display.
#[doc(alias = "errorType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Kind {
/// Error text is centered in the error applet window.
Center = ctru_sys::ERROR_TEXT,
/// Error text starts at the top of the error applet window.
Top = ctru_sys::ERROR_TEXT_WORD_WRAP,
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
}

/// Error returned by an unsuccessful [`PopUp::launch()`].
#[doc(alias = "errorReturnCode")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(i8)]
pub enum Error {
/// Unknown error occurred.
Unknown = ctru_sys::ERROR_UNKNOWN,
/// Operation not supported.
NotSupported = ctru_sys::ERROR_NOT_SUPPORTED,
/// Home button pressed while [`PopUp`] was running.
HomePressed = ctru_sys::ERROR_HOME_BUTTON,
/// Power button pressed while [`PopUp`] was running.
PowerPressed = ctru_sys::ERROR_POWER_BUTTON,
/// Reset button pressed while [`PopUp`] was running.
ResetPressed = ctru_sys::ERROR_SOFTWARE_RESET,
}

impl PopUp {
/// Initialize the error applet with the provided text kind.
#[doc(alias = "errorInit")]
pub fn new(kind: Kind) -> Self {
let mut state = Box::<errorConf>::default();
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

unsafe { ctru_sys::errorInit(state.as_mut(), kind as _, 0) };
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

Self { state }
}

/// Sets the error text to display.
#[doc(alias = "errorText")]
pub fn set_text(&mut self, text: &str) {
for (idx, code_unit) in text
.encode_utf16()
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
.chain(std::iter::once(0))
FenrirWolf marked this conversation as resolved.
Show resolved Hide resolved
.take(self.state.Text.len() - 1)
.enumerate()
{
self.state.Text[idx] = code_unit;
}
}

/// Launches the error applet.
#[doc(alias = "errorDisp")]
pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(), Error> {
unsafe { ctru_sys::errorDisp(self.state.as_mut()) }

match self.state.returnCode {
ctru_sys::ERROR_NONE | ctru_sys::ERROR_SUCCESS => Ok(()),
ctru_sys::ERROR_NOT_SUPPORTED => Err(Error::NotSupported),
ctru_sys::ERROR_HOME_BUTTON => Err(Error::HomePressed),
ctru_sys::ERROR_POWER_BUTTON => Err(Error::PowerPressed),
ctru_sys::ERROR_SOFTWARE_RESET => Err(Error::ResetPressed),
_ => Err(Error::Unknown),
}
}

/// Launches the error applet without requiring an [`Apt`] or [`Gfx`] handle.
///
/// # Safety
///
/// Causes undefined behavior if the aforementioned services are not actually active when the applet launches.
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
unsafe fn launch_unchecked(&mut self) {
FenrirWolf marked this conversation as resolved.
Show resolved Hide resolved
unsafe { ctru_sys::errorDisp(self.state.as_mut()) };
}
}

/// Sets a custom [panic hook](https://doc.rust-lang.org/std/panic/fn.set_hook.html) that uses the error applet to display panic messages.
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
///
/// You can also choose to have the previously registered panic hook called along with the error applet message, which can be useful
/// if you want to use output redirection to display panic messages over `3dslink` or `GDB`.
///
/// If the [`Gfx`] service is not initialized during a panic, the error applet will not be displayed and the old
/// panic hook will be called.
///
/// You can use [`std::panic::take_hook`](https://doc.rust-lang.org/std/panic/fn.take_hook.html) to unregister the panic hook
/// set by this function.
pub fn set_panic_hook(call_old_hook: bool) {
use crate::services::gfx::GFX_ACTIVE;
use std::sync::TryLockError;

let old_hook = std::panic::take_hook();

std::panic::set_hook(Box::new(move |panic_info| {
let thread = std::thread::current();

let name = thread.name().unwrap_or("<unnamed>");

// If we get a `WouldBlock` error, we know that the `Gfx` service has been initialized.
// Otherwise fallback to printing over stderr.
FenrirWolf marked this conversation as resolved.
Show resolved Hide resolved
if let (Err(TryLockError::WouldBlock), Ok(_apt)) = (GFX_ACTIVE.try_lock(), Apt::new()) {
if call_old_hook {
old_hook(panic_info);
}

let payload = format!("thread '{name}' {panic_info}");

let mut popup = PopUp::new(Kind::Top);

popup.set_text(&payload);

unsafe {
popup.launch_unchecked();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check my understanding, this will block the app until the popup is closed, and then the app will quit (since there's no loop or anything)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you've got that right. Though the app can technically continue if the panic is stopped by a thread boundary or catch_unwind or whatever.

}
} else {
old_hook(panic_info);
}
}));
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotSupported => write!(f, "operation not supported"),
Self::HomePressed => write!(f, "home button pressed while error applet was running"),
Self::PowerPressed => write!(f, "power button pressed while error applet was running"),
Self::ResetPressed => write!(f, "reset button pressed while error applet was running"),
Self::Unknown => write!(f, "an unknown error occurred"),
}
}
}

impl std::error::Error for Error {}
1 change: 1 addition & 0 deletions ctru-rs/src/applets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
//!
//! Applets block execution of the thread that launches them as long as the user doesn't close the applet.

pub mod error;
pub mod mii_selector;
pub mod swkbd;
2 changes: 1 addition & 1 deletion ctru-rs/src/services/gfx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ pub struct Gfx {
_service_handler: ServiceReference,
}

static GFX_ACTIVE: Mutex<()> = Mutex::new(());
pub(crate) static GFX_ACTIVE: Mutex<()> = Mutex::new(());

impl Gfx {
/// Initialize a new default service handle.
Expand Down
2 changes: 2 additions & 0 deletions ctru-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ fn main() {
.blocklist_type("u(8|16|32|64)")
.blocklist_type("__builtin_va_list")
.blocklist_type("__va_list")
.blocklist_type("errorReturnCode")
.blocklist_type("errorScreenFlag")
.opaque_type("MiiData")
.derive_default(true)
.wrap_static_fns(true)
Expand Down
18 changes: 18 additions & 0 deletions ctru-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
pub mod result;
pub use result::*;

// Fun fact: C compilers are allowed to represent enums as the smallest integer type that can hold all of its variants,
// meaning that enums are allowed to be the size of a `c_short` or a `c_char` rather than the size of a `c_int`.
// Libctru's `errorConf` struct contains two enums that depend on this narrowing property for size and alignment purposes,
// and since `bindgen` generates all enums with `c_int` sizing, we have to blocklist those types and manually define them
// here with the proper size.
Comment on lines +21 to +23
Copy link
Member

@ian-h-chamberlain ian-h-chamberlain Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really interesting! Similar to what I mentioned in #166 , maybe we can just force these constants to generate using errorReturnCode as the type instead of c_int?

To catch the struct size/alignment issues, I wonder if we could have caught that using bindgen's layout tests. I think they were omitted before because we didn't have any good way to run them, but with test-runner now we might be able to actually do it in CI. I'll file an issue (edit: #167)

Edit: bindgen also has a .fit_macro_constants(true) we could try to use for this purpose, but I'm not sure if it would help with the actual struct size.

pub const ERROR_UNKNOWN: errorReturnCode = -1;
pub const ERROR_NONE: errorReturnCode = 0;
pub const ERROR_SUCCESS: errorReturnCode = 1;
pub const ERROR_NOT_SUPPORTED: errorReturnCode = 2;
pub const ERROR_HOME_BUTTON: errorReturnCode = 10;
pub const ERROR_SOFTWARE_RESET: errorReturnCode = 11;
pub const ERROR_POWER_BUTTON: errorReturnCode = 12;
pub type errorReturnCode = libc::c_schar;
FenrirWolf marked this conversation as resolved.
Show resolved Hide resolved

pub const ERROR_NORMAL: errorScreenFlag = 0;
pub const ERROR_STEREO: errorScreenFlag = 1;
pub type errorScreenFlag = libc::c_char;

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

/// In lieu of a proper errno function exposed by libc
Expand Down
Loading