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 all 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
153 changes: 153 additions & 0 deletions ctru-rs/src/applets/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! 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>,
}

/// Determines whether the Error applet will use word wrapping when displaying a message.
#[doc(alias = "errorType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum WordWrap {
/// Error text is centered in the error applet window and does not use word wrapping.
Disabled = ctru_sys::ERROR_TEXT,
/// Error text starts at the top of the error applet window and uses word wrapping.
Enabled = ctru_sys::ERROR_TEXT_WORD_WRAP,
}

/// 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 {
/// Initializes the error applet with the provided word wrap setting.
#[doc(alias = "errorInit")]
pub fn new(word_wrap: WordWrap) -> Self {
let mut state = Box::<errorConf>::default();
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved

unsafe { ctru_sys::errorInit(state.as_mut(), word_wrap as _, 0) };

Self { state }
}

/// Sets the error text to display.
///
/// # Notes
///
/// The text will be converted to UTF-16 for display with the applet, and the message will be truncated if it exceeds
/// 1900 UTF-16 code units in length after conversion.
#[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
.take(self.state.Text.len() - 1)
.chain(std::iter::once(0))
.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 { self.launch_unchecked() }
}

/// Launches the error applet without requiring an [`Apt`] or [`Gfx`] handle.
///
/// # Safety
///
/// Potentially leads to undefined behavior if the aforementioned services are not actually active when the applet launches.
unsafe fn launch_unchecked(&mut self) -> 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),
}
}
}

/// 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 popup, which can be useful
/// if you want to use output redirection to display panic messages over `3dslink` or `GDB`.
///
/// 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.
///
/// # Notes
///
/// * 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.
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| {
// If we get a `WouldBlock` error, we know that the `Gfx` service has been initialized.
// Otherwise fallback to using the old panic hook.
if let (Err(TryLockError::WouldBlock), Ok(_apt)) = (GFX_ACTIVE.try_lock(), Apt::new()) {
if call_old_hook {
old_hook(panic_info);
}

let thread = std::thread::current();

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

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

let mut popup = PopUp::new(WordWrap::Enabled);

popup.set_text(&message);

unsafe {
let _ = popup.launch_unchecked();
}
} 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 type errorReturnCode = libc::c_schar;
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 errorScreenFlag = libc::c_char;
pub const ERROR_NORMAL: errorScreenFlag = 0;
pub const ERROR_STEREO: errorScreenFlag = 1;

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

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