diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 358f2f7..2db5b64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: matrix: toolchain: # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date - - nightly-2023-06-01 + - nightly-2024-02-18 # Check for breakage on latest nightly - nightly @@ -43,17 +43,22 @@ jobs: - name: Check formatting run: cargo fmt --all --verbose -- --check + # We always run the next two steps, so that even if formatting fails we + # still get compilation errors for the same run (mainly for matchers). + - name: Cargo check ctru-sys (without tests) run: cargo 3ds clippy --package ctru-sys --color=always --verbose + if: success() || failure() - name: Cargo check ctru-rs (including tests) run: cargo 3ds clippy --package ctru-rs --color=always --verbose --all-targets + if: success() || failure() test: strategy: matrix: toolchain: - - nightly-2023-06-01 + - nightly-2024-02-18 - nightly continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ubuntu-latest diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index b6f7ce5..9583c21 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -10,7 +10,7 @@ categories = ["os", "api-bindings", "hardware-support"] exclude = ["examples"] license = "Zlib" edition = "2021" -rust-version = "1.70" +rust-version = "1.73" [lib] crate-type = ["rlib"] @@ -25,6 +25,7 @@ pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } libc = "0.2.121" bitflags = "2.3.3" macaddr = "1.0.1" +widestring = "1.0.2" [build-dependencies] toml = "0.5" diff --git a/ctru-rs/examples/file-explorer.rs b/ctru-rs/examples/file-explorer.rs index 5c158ac..14f3474 100644 --- a/ctru-rs/examples/file-explorer.rs +++ b/ctru-rs/examples/file-explorer.rs @@ -165,7 +165,7 @@ impl<'a> FileExplorer<'a> { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { let mut keyboard = SoftwareKeyboard::default(); - match keyboard.get_string(2048, self.apt, self.gfx) { + match keyboard.launch(self.apt, self.gfx) { Ok((path, Button::Right)) => { // Clicked "OK". action(self, path); diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index 1d24a7d..40635de 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -5,8 +5,6 @@ use ctru::applets::swkbd::{Button, CallbackResult, SoftwareKeyboard}; use ctru::prelude::*; -use std::ffi::CString; - fn main() { let apt = Apt::new().unwrap(); let mut hid = Hid::new().unwrap(); @@ -21,13 +19,9 @@ fn main() { // Custom filter callback to handle the given input. // Using this callback it's possible to integrate the applet // with custom error messages when the input is incorrect. - keyboard.set_filter_callback(Some(Box::new(|str| { - // The string is guaranteed to contain valid Unicode text, so we can safely unwrap and use it as a normal `&str`. - if str.to_str().unwrap().contains("boo") { - return ( - CallbackResult::Retry, - Some(CString::new("Ah, you scared me!").unwrap()), - ); + keyboard.set_filter_callback(Some(Box::new(move |str| { + if str.contains("boo") { + return (CallbackResult::Retry, Some("Ah, you scared me!".into())); } (CallbackResult::Ok, None) @@ -44,9 +38,9 @@ fn main() { // Check if the user request to write some input. if hid.keys_down().contains(KeyPad::A) { - // Raise the software keyboard. You can perform different actions depending on which + // Launch the software keyboard. You can perform different actions depending on which // software button the user pressed. - match keyboard.get_string(2048, &apt, &gfx) { + match keyboard.launch(&apt, &gfx) { Ok((text, Button::Right)) => println!("You entered: {text}"), Ok((_, Button::Left)) => println!("Cancelled"), Ok((_, Button::Middle)) => println!("How did you even press this?"), diff --git a/ctru-rs/src/applets/error.rs b/ctru-rs/src/applets/error.rs new file mode 100644 index 0000000..84dc3e7 --- /dev/null +++ b/ctru-rs/src/applets/error.rs @@ -0,0 +1,153 @@ +//! Error applet. +//! +//! This applet displays error text as a pop-up message on the lower screen. + +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 { + state: Box, +} + +/// 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::::default(); + + 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() + .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. +/// +/// 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(""); + + 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 {} diff --git a/ctru-rs/src/applets/mod.rs b/ctru-rs/src/applets/mod.rs index 244fb74..838ef6b 100644 --- a/ctru-rs/src/applets/mod.rs +++ b/ctru-rs/src/applets/mod.rs @@ -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; diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 41d40cd..c3b49fc 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -4,24 +4,27 @@ #![doc(alias = "keyboard")] use crate::services::{apt::Apt, gfx::Gfx}; -use ctru_sys::{self, SwkbdState}; +use ctru_sys::{ + aptLaunchLibraryApplet, aptSetMessageCallback, envGetAptAppId, svcCloseHandle, + svcCreateMemoryBlock, APT_SendParameter, SwkbdButton, SwkbdDictWord, SwkbdLearningData, + SwkbdState, SwkbdStatusData, APPID_SOFTWARE_KEYBOARD, APTCMD_MESSAGE, NS_APPID, +}; use bitflags::bitflags; -use libc; -use std::ffi::{CStr, CString}; +use std::borrow::Cow; use std::fmt::Display; use std::iter::once; use std::str; -type CallbackFunction = dyn Fn(&CStr) -> (CallbackResult, Option); +type CallbackFunction = dyn Fn(&str) -> (CallbackResult, Option>); /// Configuration structure to setup the Software Keyboard applet. #[doc(alias = "SwkbdState")] pub struct SoftwareKeyboard { state: Box, - callback: Option>, - error_message: Option, + filter_callback: Option>, + initial_text: Option>, } /// Configuration structure to setup the Parental Lock applet. @@ -109,7 +112,7 @@ pub enum ButtonConfig { LeftMiddleRight = 3, } -/// Error returned by an unsuccessful [`SoftwareKeyboard::get_string()`]. +/// Error returned by an unsuccessful [`SoftwareKeyboard::launch()`]. #[doc(alias = "SwkbdResult")] #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(i32)] @@ -206,6 +209,13 @@ bitflags! { } } +// Internal book-keeping struct used to send data to `aptSetMessageCallback` when calling the software keyboard. +#[derive(Copy, Clone)] +struct MessageCallbackData { + filter_callback: *const Box, + swkbd_shared_mem_ptr: *mut libc::c_void, +} + impl SoftwareKeyboard { /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3). /// @@ -231,71 +241,14 @@ impl SoftwareKeyboard { ctru_sys::swkbdInit(state.as_mut(), keyboard_type.into(), buttons.into(), -1); Self { state, - callback: None, - error_message: None, + filter_callback: None, + initial_text: None, } } } /// Launches the applet based on the given configuration and returns a string containing the text input. /// - /// # Notes - /// - /// The text received from the keyboard will be truncated if it is longer than `max_bytes`. - /// Use [`SoftwareKeyboard::set_max_text_len()`] to make sure the buffer can contain the input text. - /// - /// # Example - /// - /// ``` - /// # let _runner = test_runner::GdbRunner::default(); - /// # use std::error::Error; - /// # fn main() -> Result<(), Box> { - /// # use ctru::services::{apt::Apt, gfx::Gfx}; - /// # - /// # let gfx = Gfx::new().unwrap(); - /// # let apt = Apt::new().unwrap(); - /// # - /// use ctru::applets::swkbd::SoftwareKeyboard; - /// let mut keyboard = SoftwareKeyboard::default(); - /// - /// let (text, button) = keyboard.get_string(2048, &apt, &gfx)?; - /// # - /// # Ok(()) - /// # } - /// ``` - #[doc(alias = "swkbdInputText")] - pub fn get_string( - &mut self, - max_bytes: usize, - apt: &Apt, - gfx: &Gfx, - ) -> Result<(String, Button), Error> { - // Unfortunately the libctru API doesn't really provide a way to get the exact length - // of the string that it receieves from the software keyboard. Instead it expects you - // to pass in a buffer and hope that it's big enough to fit the entire string, so - // you have to set some upper limit on the potential size of the user's input. - let mut tmp = vec![0u8; max_bytes]; - let button = self.write_exact(&mut tmp, apt, gfx)?; - - // libctru does, however, seem to ensure that the buffer will always contain a properly - // terminated UTF-8 sequence even if the input has to be truncated, so these operations - // should be safe. - let len = unsafe { libc::strlen(tmp.as_ptr()) }; - tmp.truncate(len); - - let res = unsafe { String::from_utf8_unchecked(tmp) }; - - Ok((res, button)) - } - - /// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from - /// this software keyboard. - /// - /// # Notes - /// - /// If the buffer is too small to contain the entire sequence received from the keyboard, - /// the output will be truncated. - /// /// # Example /// /// ``` @@ -310,30 +263,21 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// let mut buffer = vec![0; 100]; - /// - /// let button = keyboard.write_exact(&mut buffer, &apt, &gfx)?; + /// let (text, button) = keyboard.launch(&apt, &gfx)?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "swkbdInputText")] - pub fn write_exact(&mut self, buf: &mut [u8], _apt: &Apt, _gfx: &Gfx) -> Result { - unsafe { - // The filter callback gets reset every time the SoftwareKeyboard is used. - ctru_sys::swkbdSetFilterCallback( - self.state.as_mut(), - Some(Self::internal_callback), - (self as *mut Self).cast(), - ); - - match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { - ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), - ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), - ctru_sys::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), - ctru_sys::SWKBD_BUTTON_RIGHT => Ok(Button::Right), - _ => unreachable!(), - } + pub fn launch(&mut self, apt: &Apt, gfx: &Gfx) -> Result<(String, Button), Error> { + let mut output = String::new(); + + match self.swkbd_input_text(&mut output, apt, gfx) { + ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), + ctru_sys::SWKBD_BUTTON_LEFT => Ok((output, Button::Left)), + ctru_sys::SWKBD_BUTTON_MIDDLE => Ok((output, Button::Middle)), + ctru_sys::SWKBD_BUTTON_RIGHT => Ok((output, Button::Right)), + _ => unreachable!(), } } @@ -399,17 +343,13 @@ impl SoftwareKeyboard { /// # fn main() { /// # /// use std::borrow::Cow; - /// use std::ffi::CString; /// use ctru::applets::swkbd::{SoftwareKeyboard, CallbackResult}; /// /// let mut keyboard = SoftwareKeyboard::default(); /// - /// keyboard.set_filter_callback(Some(Box::new(|str| { - /// if str.to_str().unwrap().contains("boo") { - /// return ( - /// CallbackResult::Retry, - /// Some(CString::new("Ah, you scared me!").unwrap()), - /// ); + /// keyboard.set_filter_callback(Some(Box::new(move |str| { + /// if str.contains("boo") { + /// return (CallbackResult::Retry, Some("Ah, you scared me!".into())); /// } /// /// (CallbackResult::Ok, None) @@ -417,45 +357,7 @@ impl SoftwareKeyboard { /// # /// # } pub fn set_filter_callback(&mut self, callback: Option>) { - self.callback = callback; - } - - /// Internal function called by the filter callback. - extern "C" fn internal_callback( - user: *mut libc::c_void, - pp_message: *mut *const libc::c_char, - text: *const libc::c_char, - _text_size: libc::size_t, - ) -> ctru_sys::SwkbdCallbackResult { - let this: *mut SoftwareKeyboard = user.cast(); - - unsafe { - // Reset any leftover error message. - (*this).error_message = None; - - let text = CStr::from_ptr(text); - - let result = { - // Run the callback if still available. - if let Some(callback) = &mut (*this).callback { - let (res, cstr) = callback(text); - - // Due to how `libctru` operates, the user is expected to keep the error message alive until - // the end of the Software Keyboard prompt. We ensure that happens by saving it within the configuration. - (*this).error_message = cstr; - - if let Some(newstr) = &(*this).error_message { - *pp_message = newstr.as_ptr(); - } - - res - } else { - CallbackResult::Ok - } - }; - - result.into() - } + self.filter_callback = callback; } /// Configure the maximum number of digits that can be entered in the keyboard when the [`Filters::DIGITS`] flag is enabled. @@ -488,6 +390,10 @@ impl SoftwareKeyboard { /// /// The initial text is the text already written when you open the software keyboard. /// + /// # Notes + /// + /// Passing [`None`] will clear the initial text. + /// /// # Example /// /// ``` @@ -497,21 +403,25 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// keyboard.set_initial_text("Write here what you like!"); + /// keyboard.set_initial_text(Some("Write here what you like!".into())); /// # /// # } #[doc(alias = "swkbdSetInitialText")] - pub fn set_initial_text(&mut self, text: &str) { - unsafe { - let nul_terminated: String = text.chars().chain(once('\0')).collect(); - ctru_sys::swkbdSetInitialText(self.state.as_mut(), nul_terminated.as_ptr()); - } + pub fn set_initial_text(&mut self, text: Option>) { + self.initial_text = text; } /// Set the hint text for this software keyboard. /// /// The hint text is the text shown in gray before any text gets written in the input box. /// + /// # Notes + /// + /// Passing [`None`] will clear the hint text. + /// + /// The hint text will be converted to UTF-16 when passed to the software keyboard, and the text will be truncated + /// if the length exceeds 64 code units after conversion. + /// /// # Example /// /// ``` @@ -521,14 +431,22 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// keyboard.set_hint_text("Write here what you like!"); + /// keyboard.set_hint_text(Some("Write here what you like!")); /// # /// # } #[doc(alias = "swkbdSetHintText")] - pub fn set_hint_text(&mut self, text: &str) { - unsafe { - let nul_terminated: String = text.chars().chain(once('\0')).collect(); - ctru_sys::swkbdSetHintText(self.state.as_mut(), nul_terminated.as_ptr()); + pub fn set_hint_text(&mut self, text: Option<&str>) { + if let Some(text) = text { + for (idx, code_unit) in text + .encode_utf16() + .take(self.state.hint_text.len() - 1) + .chain(once(0)) + .enumerate() + { + self.state.hint_text[idx] = code_unit; + } + } else { + self.state.hint_text[0] = 0; } } @@ -623,15 +541,18 @@ impl SoftwareKeyboard { /// # } #[doc(alias = "swkbdSetButton")] pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) { - unsafe { - let nul_terminated: String = text.chars().chain(once('\0')).collect(); - ctru_sys::swkbdSetButton( - self.state.as_mut(), - button.into(), - nul_terminated.as_ptr(), - submit, - ); + let button_text = &mut self.state.button_text[button as usize]; + + for (idx, code_unit) in text + .encode_utf16() + .take(button_text.len() - 1) + .chain(once(0)) + .enumerate() + { + button_text[idx] = code_unit; } + + self.state.button_submits_text[button as usize] = submit; } /// Configure the maximum number of UTF-16 code units that can be entered into the software @@ -643,7 +564,7 @@ impl SoftwareKeyboard { /// /// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, /// so this code point limit does not necessarily equal the max number of UTF-8 code points - /// receivable by [`SoftwareKeyboard::get_string()`] and [`SoftwareKeyboard::write_exact()`]. + /// receivable by [`SoftwareKeyboard::launch()`]. /// /// # Example /// @@ -664,6 +585,276 @@ impl SoftwareKeyboard { // Activate the specific validation rule for maximum length. self.state.valid_input = ValidInput::FixedLen.into(); } + + // A reimplementation of `swkbdInputText` from `libctru/source/applets/swkbd.c`. Allows us to fix various + // API nits and get rid of awkward type conversions when interacting with the Software Keyboard. + fn swkbd_input_text(&mut self, output: &mut String, _apt: &Apt, _gfx: &Gfx) -> SwkbdButton { + use ctru_sys::{ + MEMPERM_READ, MEMPERM_WRITE, R_FAILED, SWKBD_BUTTON_LEFT, SWKBD_BUTTON_MIDDLE, + SWKBD_BUTTON_NONE, SWKBD_BUTTON_RIGHT, SWKBD_D0_CLICK, SWKBD_D1_CLICK0, + SWKBD_D1_CLICK1, SWKBD_D2_CLICK0, SWKBD_D2_CLICK1, SWKBD_D2_CLICK2, + SWKBD_FILTER_CALLBACK, SWKBD_OUTOFMEM, + }; + + let swkbd = self.state.as_mut(); + let extra = unsafe { swkbd.__bindgen_anon_1.extra }; + + // Calculate shared mem size + let mut shared_mem_size = 0; + + shared_mem_size += (std::mem::size_of::() * (swkbd.max_text_len as usize + 1)) + .next_multiple_of(std::mem::size_of::()); + + let dict_off = shared_mem_size; + + shared_mem_size += (std::mem::size_of::() * swkbd.dict_word_count as usize) + .next_multiple_of(std::mem::size_of::()); + + let status_off = shared_mem_size; + + shared_mem_size += if swkbd.initial_learning_offset >= 0 { + std::mem::size_of::() + } else { + 0 + }; + + let learning_off = shared_mem_size; + + shared_mem_size += if swkbd.initial_learning_offset >= 0 { + std::mem::size_of::() + } else { + 0 + }; + + if swkbd.save_state_flags & (1 << 0) != 0 { + swkbd.status_offset = shared_mem_size as _; + shared_mem_size += std::mem::size_of::(); + } + + if swkbd.save_state_flags & (1 << 1) != 0 { + swkbd.learning_offset = shared_mem_size as _; + shared_mem_size += std::mem::size_of::(); + } + + shared_mem_size = shared_mem_size.next_multiple_of(0x1000); + + swkbd.shared_memory_size = shared_mem_size; + + // Allocate shared mem + let swkbd_shared_mem_ptr = unsafe { libc::memalign(0x1000, shared_mem_size) }; + + let mut swkbd_shared_mem_handle = 0; + + if swkbd_shared_mem_ptr.is_null() { + swkbd.result = SWKBD_OUTOFMEM; + return SWKBD_BUTTON_NONE; + } + + let res = unsafe { + svcCreateMemoryBlock( + &mut swkbd_shared_mem_handle, + swkbd_shared_mem_ptr as _, + shared_mem_size as _, + MEMPERM_READ | MEMPERM_WRITE, + MEMPERM_READ | MEMPERM_WRITE, + ) + }; + + if R_FAILED(res) { + unsafe { + libc::free(swkbd_shared_mem_ptr); + swkbd.result = SWKBD_OUTOFMEM; + return SWKBD_BUTTON_NONE; + } + } + + // Copy stuff to shared mem + if let Some(initial_text) = self.initial_text.as_deref() { + swkbd.initial_text_offset = 0; + + let mut initial_text_cursor = swkbd_shared_mem_ptr.cast(); + + for code_unit in initial_text + .encode_utf16() + .take(swkbd.max_text_len as _) + .chain(once(0)) + { + unsafe { + *initial_text_cursor = code_unit; + initial_text_cursor = initial_text_cursor.add(1); + } + } + } + + if !extra.dict.is_null() { + swkbd.dict_offset = dict_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.dict, + swkbd_shared_mem_ptr.add(dict_off).cast(), + swkbd.dict_word_count as _, + ) + }; + } + + if swkbd.initial_status_offset >= 0 { + swkbd.initial_status_offset = status_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.status_data, + swkbd_shared_mem_ptr.add(status_off).cast(), + 1, + ) + }; + } + + if swkbd.initial_learning_offset >= 0 { + swkbd.initial_learning_offset = learning_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.learning_data, + swkbd_shared_mem_ptr.add(learning_off).cast(), + 1, + ) + }; + } + + if self.filter_callback.is_some() { + swkbd.filter_flags |= SWKBD_FILTER_CALLBACK; + } else { + swkbd.filter_flags &= !SWKBD_FILTER_CALLBACK; + } + + // Launch swkbd + unsafe { + swkbd.__bindgen_anon_1.reserved.fill(0); + + // We need to pass a thin pointer to the boxed closure over FFI. Since we know that the message callback will finish before + // `self` is allowed to be moved again, we can safely use a pointer to the local value contained in `self.filter_callback` + // The cast here is also sound since the pointer will only be read from if `self.filter_callback.is_some()` returns true. + let mut data = MessageCallbackData { + filter_callback: std::ptr::addr_of!(self.filter_callback).cast(), + swkbd_shared_mem_ptr, + }; + + if self.filter_callback.is_some() { + aptSetMessageCallback( + Some(Self::swkbd_message_callback), + std::ptr::addr_of_mut!(data).cast(), + ) + } + + aptLaunchLibraryApplet( + APPID_SOFTWARE_KEYBOARD, + (swkbd as *mut SwkbdState).cast(), + std::mem::size_of::(), + swkbd_shared_mem_handle, + ); + + if self.filter_callback.is_some() { + aptSetMessageCallback(None, std::ptr::null_mut()); + } + + let _ = svcCloseHandle(swkbd_shared_mem_handle); + } + + let button = match swkbd.result { + SWKBD_D1_CLICK0 | SWKBD_D2_CLICK0 => SWKBD_BUTTON_LEFT, + SWKBD_D2_CLICK1 => SWKBD_BUTTON_MIDDLE, + SWKBD_D0_CLICK | SWKBD_D1_CLICK1 | SWKBD_D2_CLICK2 => SWKBD_BUTTON_RIGHT, + _ => SWKBD_BUTTON_NONE, + }; + + if swkbd.text_length > 0 { + let text16 = unsafe { + widestring::Utf16Str::from_slice_unchecked(std::slice::from_raw_parts( + swkbd_shared_mem_ptr.add(swkbd.text_offset as _).cast(), + swkbd.text_length as _, + )) + }; + + *output = text16.to_string(); + } + + if swkbd.save_state_flags & (1 << 0) != 0 { + unsafe { + std::ptr::copy_nonoverlapping( + swkbd_shared_mem_ptr.add(swkbd.status_offset as _).cast(), + extra.status_data, + 1, + ) + }; + } + + if swkbd.save_state_flags & (1 << 1) != 0 { + unsafe { + std::ptr::copy_nonoverlapping( + swkbd_shared_mem_ptr.add(swkbd.learning_offset as _).cast(), + extra.learning_data, + 1, + ) + }; + } + + unsafe { libc::free(swkbd_shared_mem_ptr) }; + + button + } + + // A reimplementation of `swkbdMessageCallback` from `libctru/source/applets/swkbd.c`. + // This function sets up and then calls the filter callback + unsafe extern "C" fn swkbd_message_callback( + user: *mut libc::c_void, + sender: NS_APPID, + msg: *mut libc::c_void, + msg_size: libc::size_t, + ) { + if sender != ctru_sys::APPID_SOFTWARE_KEYBOARD + || msg_size != std::mem::size_of::() + { + return; + } + + let swkbd = unsafe { &mut *msg.cast::() }; + let data = unsafe { *user.cast::() }; + + let text16 = unsafe { + widestring::Utf16Str::from_slice_unchecked(std::slice::from_raw_parts( + data.swkbd_shared_mem_ptr.add(swkbd.text_offset as _).cast(), + swkbd.text_length as _, + )) + }; + + let text8 = text16.to_string(); + + let filter_callback = unsafe { &**data.filter_callback }; + + let (result, retmsg) = filter_callback(&text8); + + swkbd.callback_result = result as _; + + if let Some(msg) = retmsg.as_deref() { + for (idx, code_unit) in msg + .encode_utf16() + .take(swkbd.callback_msg.len() - 1) + .chain(once(0)) + .enumerate() + { + swkbd.callback_msg[idx] = code_unit; + } + } + + let _ = unsafe { + APT_SendParameter( + envGetAptAppId(), + sender, + APTCMD_MESSAGE, + (swkbd as *mut SwkbdState).cast(), + std::mem::size_of::() as _, + 0, + ) + }; + } } impl ParentalLock { diff --git a/ctru-rs/src/console.rs b/ctru-rs/src/console.rs index cb98840..02702cc 100644 --- a/ctru-rs/src/console.rs +++ b/ctru-rs/src/console.rs @@ -5,8 +5,7 @@ //! //! Have a look at [`Soc::redirect_to_3dslink()`](crate::services::soc::Soc::redirect_to_3dslink) for a better alternative when debugging applications. -use std::cell::RefMut; -use std::default::Default; +use std::cell::{RefMut, UnsafeCell}; use ctru_sys::{consoleClear, consoleInit, consoleSelect, consoleSetWindow, PrintConsole}; @@ -63,7 +62,7 @@ impl ConsoleScreen for S {} /// More info in the [`cargo-3ds` docs](https://github.com/rust3ds/cargo-3ds#running-executables). #[doc(alias = "PrintConsole")] pub struct Console<'screen> { - context: Box, + context: Box>, screen: RefMut<'screen, dyn ConsoleScreen>, } @@ -107,9 +106,9 @@ impl<'screen> Console<'screen> { /// ``` #[doc(alias = "consoleInit")] pub fn new(screen: RefMut<'screen, S>) -> Self { - let mut context = Box::::default(); + let context = Box::>::default(); - unsafe { consoleInit(screen.as_raw(), context.as_mut()) }; + unsafe { consoleInit(screen.as_raw(), context.get()) }; Console { context, screen } } @@ -143,7 +142,7 @@ impl<'screen> Console<'screen> { /// ``` pub fn exists() -> bool { unsafe { - let current_console = ctru_sys::consoleSelect(&mut EMPTY_CONSOLE); + let current_console = ctru_sys::consoleSelect(std::ptr::addr_of_mut!(EMPTY_CONSOLE)); let res = (*current_console).consoleInitialised; @@ -190,7 +189,7 @@ impl<'screen> Console<'screen> { #[doc(alias = "consoleSelect")] pub fn select(&self) { unsafe { - consoleSelect(self.context.as_ref() as *const _ as *mut _); + consoleSelect(self.context.get()); } } @@ -248,7 +247,7 @@ impl<'screen> Console<'screen> { unsafe { consoleSetWindow( - self.context.as_mut(), + self.context.get(), x.into(), y.into(), width.into(), @@ -338,7 +337,10 @@ impl Swap for Console<'_> { /// This should be called once per frame at most. fn swap_buffers(&mut self) { self.screen.swap_buffers(); - self.context.frameBuffer = self.screen.raw_framebuffer().ptr as *mut u16; + + unsafe { + (*self.context.get()).frameBuffer = self.screen.raw_framebuffer().ptr as *mut u16 + }; } fn set_double_buffering(&mut self, enabled: bool) { @@ -364,9 +366,9 @@ impl Drop for Console<'_> { // the screen, but it won't crash either. // Get the current console by replacing it with an empty one. - let current_console = ctru_sys::consoleSelect(&mut EMPTY_CONSOLE); + let current_console = ctru_sys::consoleSelect(std::ptr::addr_of_mut!(EMPTY_CONSOLE)); - if std::ptr::eq(current_console, &*self.context) { + if std::ptr::eq(current_console, self.context.get()) { // Console dropped while selected. We just replaced it with the // empty console so nothing more to do. } else { diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index 3aa9313..8000858 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -18,6 +18,7 @@ #![crate_type = "rlib"] #![crate_name = "ctru"] #![warn(missing_docs)] +#![deny(unsafe_op_in_unsafe_fn)] #![feature(custom_test_frameworks)] #![feature(try_trait_v2)] #![feature(allocator_api)] diff --git a/ctru-rs/src/linear.rs b/ctru-rs/src/linear.rs index 927d556..548f0ad 100644 --- a/ctru-rs/src/linear.rs +++ b/ctru-rs/src/linear.rs @@ -42,6 +42,8 @@ unsafe impl Allocator for LinearAllocator { #[doc(alias = "linearFree")] unsafe fn deallocate(&self, ptr: NonNull, _layout: Layout) { - ctru_sys::linearFree(ptr.as_ptr().cast()); + unsafe { + ctru_sys::linearFree(ptr.as_ptr().cast()); + } } } diff --git a/ctru-rs/src/services/am.rs b/ctru-rs/src/services/am.rs index f4b6e6a..2bcd47a 100644 --- a/ctru-rs/src/services/am.rs +++ b/ctru-rs/src/services/am.rs @@ -51,6 +51,11 @@ impl<'a> Title<'a> { pub fn version(&self) -> u16 { self.version } + + /// Returns this title's media type + pub fn media_type(&self) -> MediaType { + self.mediatype + } } /// Handle to the Application Manager service. diff --git a/ctru-rs/src/services/apt.rs b/ctru-rs/src/services/apt.rs index 89ad255..e1f1f57 100644 --- a/ctru-rs/src/services/apt.rs +++ b/ctru-rs/src/services/apt.rs @@ -82,6 +82,48 @@ impl Apt { Ok(()) } } + + /// Set if the console is allowed to enter sleep mode. + /// + /// You can check whether the console is allowed to sleep with [Apt::is_sleep_allowed]. + #[doc(alias = "aptSetSleepAllowed")] + pub fn set_sleep_allowed(&mut self, allowed: bool) { + unsafe { + ctru_sys::aptSetSleepAllowed(allowed); + } + } + + /// Check if the console is allowed to enter sleep mode. + /// + /// You can set whether the console is allowed to sleep with [Apt::set_sleep_allowed]. + #[doc(alias = "aptIsSleepAllowed")] + pub fn is_sleep_allowed(&self) -> bool { + unsafe { ctru_sys::aptIsSleepAllowed() } + } + + /// Set if the console is allowed to enter the home menu. + /// + /// You can check whether the console is allowed to enter the home menu with [Apt::is_home_allowed]. + #[doc(alias = "aptSetHomeAllowed")] + pub fn set_home_allowed(&mut self, allowed: bool) { + unsafe { + ctru_sys::aptSetHomeAllowed(allowed); + } + } + + /// Check if the console is allowed to enter the home menu. + /// + /// You can set whether the console is allowed to enter the home menu with [Apt::set_home_allowed]. + #[doc(alias = "aptIsHomeAllowed")] + pub fn is_home_allowed(&self) -> bool { + unsafe { ctru_sys::aptIsHomeAllowed() } + } + + /// Immediately jumps to the home menu. + #[doc(alias = "aptJumpToHomeMenu")] + pub fn jump_to_home_menu(&mut self) { + unsafe { ctru_sys::aptJumpToHomeMenu() } + } } impl Drop for Apt { @@ -90,3 +132,48 @@ impl Drop for Apt { unsafe { ctru_sys::aptExit() }; } } + +/// Can launch other applications when the current one exits. +pub struct Chainloader<'a> { + _apt: &'a Apt, +} + +impl<'a> Chainloader<'a> { + /// Gets a handle to the chainloader + pub fn new(apt: &'a Apt) -> Self { + Self { _apt: apt } + } + + /// Checks if the chainloader is set + #[doc(alias = "aptIsChainload")] + pub fn is_set(&self) -> bool { + // static funtion not exported + unsafe { (ctru_sys::envGetSystemRunFlags() & ctru_sys::RUNFLAG_APTCHAINLOAD) != 0 } + } + + /// Clears the chainloader state. + #[doc(alias = "aptClearChainloader")] + pub fn clear(&mut self) { + unsafe { ctru_sys::aptClearChainloader() } + } + + /// Configures the chainloader to launch a specific application. + /// + /// See also [`Title`](crate::services::am::Title] + #[doc(alias = "aptSetChainloader")] + pub fn set(&mut self, title: &super::am::Title<'_>) { + unsafe { ctru_sys::aptSetChainloader(title.id(), title.media_type() as u8) } + } + + /// Configures the chainloader to launch the previous application. + #[doc(alias = "aptSetChainloaderToCaller")] + pub fn set_to_caller(&mut self) { + unsafe { ctru_sys::aptSetChainloaderToCaller() } + } + + /// Configures the chainloader to relaunch the current application (i.e. soft-reset) + #[doc(alias = "aptSetChainloaderToSelf")] + pub fn set_to_self(&mut self) { + unsafe { ctru_sys::aptSetChainloaderToSelf() } + } +} diff --git a/ctru-rs/src/services/gfx.rs b/ctru-rs/src/services/gfx.rs index 7a6ec6e..9799665 100644 --- a/ctru-rs/src/services/gfx.rs +++ b/ctru-rs/src/services/gfx.rs @@ -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. diff --git a/ctru-rs/src/services/ir_user.rs b/ctru-rs/src/services/ir_user.rs index 7f1bd84..9c92f2e 100644 --- a/ctru-rs/src/services/ir_user.rs +++ b/ctru-rs/src/services/ir_user.rs @@ -347,9 +347,11 @@ impl IrUser { let mut shared_mem_guard = IR_USER_STATE.lock().unwrap(); let shared_mem = shared_mem_guard.as_mut().unwrap(); - shared_mem - .service_handle - .send_service_request(request, expected_response_len) + unsafe { + shared_mem + .service_handle + .send_service_request(request, expected_response_len) + } } } diff --git a/ctru-rs/src/services/ndsp/mod.rs b/ctru-rs/src/services/ndsp/mod.rs index 3e96184..beb3772 100644 --- a/ctru-rs/src/services/ndsp/mod.rs +++ b/ctru-rs/src/services/ndsp/mod.rs @@ -21,7 +21,6 @@ use crate::error::ResultCode; use crate::services::ServiceReference; use std::cell::{RefCell, RefMut}; -use std::default::Default; use std::error; use std::fmt; use std::sync::Mutex; diff --git a/ctru-rs/src/services/svc.rs b/ctru-rs/src/services/svc.rs index 11530f0..9b036a3 100644 --- a/ctru-rs/src/services/svc.rs +++ b/ctru-rs/src/services/svc.rs @@ -50,19 +50,28 @@ impl HandleExt for Handle { ) -> crate::Result> { // Copy over the request let cmd_buffer_ptr = unsafe { ctru_sys::getThreadCommandBuffer() }; - std::ptr::copy_nonoverlapping(request.as_ptr(), cmd_buffer_ptr, request.len()); - // Send the request - ResultCode(ctru_sys::svcSendSyncRequest(self))?; + unsafe { + std::ptr::copy_nonoverlapping(request.as_ptr(), cmd_buffer_ptr, request.len()); + + // Send the request + ResultCode(ctru_sys::svcSendSyncRequest(self))?; - // Handle the result returned by the service - let result = unsafe { std::ptr::read(cmd_buffer_ptr.add(1)) }; - ResultCode(result as ctru_sys::Result)?; + // Handle the result returned by the service + let result = std::ptr::read(cmd_buffer_ptr.add(1)); + ResultCode(result as ctru_sys::Result)?; + } // Copy back the response request.clear(); request.resize(expected_response_len, 0); - std::ptr::copy_nonoverlapping(cmd_buffer_ptr, request.as_mut_ptr(), expected_response_len); + unsafe { + std::ptr::copy_nonoverlapping( + cmd_buffer_ptr, + request.as_mut_ptr(), + expected_response_len, + ); + } Ok(request) } diff --git a/ctru-sys/build.rs b/ctru-sys/build.rs index 5e71ae0..51950c1 100644 --- a/ctru-sys/build.rs +++ b/ctru-sys/build.rs @@ -73,12 +73,14 @@ fn main() { .use_core() .trust_clang_mangling(false) .must_use_type("Result") - .layout_tests(false) + .layout_tests(true) .ctypes_prefix("::libc") .prepend_enum_name(false) .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) diff --git a/ctru-sys/src/lib.rs b/ctru-sys/src/lib.rs index b9f71de..5a60de2 100644 --- a/ctru-sys/src/lib.rs +++ b/ctru-sys/src/lib.rs @@ -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. +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