Browse Source

Merge branch 'feature/uds' of github.com:Jhynjhiruu/ctru-rs into feature/uds

pull/156/head
Jhynjhiruu 9 months ago
parent
commit
fe85aa2cfb
  1. 9
      .github/workflows/ci.yml
  2. 3
      ctru-rs/Cargo.toml
  3. 2
      ctru-rs/examples/file-explorer.rs
  4. 16
      ctru-rs/examples/software-keyboard.rs
  5. 153
      ctru-rs/src/applets/error.rs
  6. 1
      ctru-rs/src/applets/mod.rs
  7. 493
      ctru-rs/src/applets/swkbd.rs
  8. 24
      ctru-rs/src/console.rs
  9. 1
      ctru-rs/src/lib.rs
  10. 4
      ctru-rs/src/linear.rs
  11. 5
      ctru-rs/src/services/am.rs
  12. 87
      ctru-rs/src/services/apt.rs
  13. 2
      ctru-rs/src/services/gfx.rs
  14. 8
      ctru-rs/src/services/ir_user.rs
  15. 1
      ctru-rs/src/services/ndsp/mod.rs
  16. 23
      ctru-rs/src/services/svc.rs
  17. 2
      ctru-rs/src/services/uds.rs
  18. 4
      ctru-sys/build.rs
  19. 18
      ctru-sys/src/lib.rs

9
.github/workflows/ci.yml

@ -15,7 +15,7 @@ jobs: @@ -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: @@ -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

3
ctru-rs/Cargo.toml

@ -10,7 +10,7 @@ categories = ["os", "api-bindings", "hardware-support"] @@ -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" } @@ -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"

2
ctru-rs/examples/file-explorer.rs

@ -165,7 +165,7 @@ impl<'a> FileExplorer<'a> { @@ -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);

16
ctru-rs/examples/software-keyboard.rs

@ -5,8 +5,6 @@ @@ -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() { @@ -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() { @@ -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?"),

153
ctru-rs/src/applets/error.rs

@ -0,0 +1,153 @@ @@ -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<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();
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("<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
ctru-rs/src/applets/mod.rs

@ -8,5 +8,6 @@ @@ -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;

493
ctru-rs/src/applets/swkbd.rs

@ -4,24 +4,27 @@ @@ -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<CString>);
type CallbackFunction = dyn Fn(&str) -> (CallbackResult, Option<Cow<'static, str>>);
/// Configuration structure to setup the Software Keyboard applet.
#[doc(alias = "SwkbdState")]
pub struct SoftwareKeyboard {
state: Box<SwkbdState>,
callback: Option<Box<CallbackFunction>>,
error_message: Option<CString>,
filter_callback: Option<Box<CallbackFunction>>,
initial_text: Option<Cow<'static, str>>,
}
/// Configuration structure to setup the Parental Lock applet.
@ -109,7 +112,7 @@ pub enum ButtonConfig { @@ -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! { @@ -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<CallbackFunction>,
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 { @@ -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<dyn Error>> {
/// # 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 { @@ -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<Button, Error> {
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 { @@ -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 { @@ -417,45 +357,7 @@ impl SoftwareKeyboard {
/// #
/// # }
pub fn set_filter_callback(&mut self, callback: Option<Box<CallbackFunction>>) {
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 { @@ -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 { @@ -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<Cow<'static, str>>) {
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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::<u16>() * (swkbd.max_text_len as usize + 1))
.next_multiple_of(std::mem::size_of::<usize>());
let dict_off = shared_mem_size;
shared_mem_size += (std::mem::size_of::<SwkbdDictWord>() * swkbd.dict_word_count as usize)
.next_multiple_of(std::mem::size_of::<usize>());
let status_off = shared_mem_size;
shared_mem_size += if swkbd.initial_learning_offset >= 0 {
std::mem::size_of::<SwkbdStatusData>()
} else {
0
};
let learning_off = shared_mem_size;
shared_mem_size += if swkbd.initial_learning_offset >= 0 {
std::mem::size_of::<SwkbdLearningData>()
} else {
0
};
if swkbd.save_state_flags & (1 << 0) != 0 {
swkbd.status_offset = shared_mem_size as _;
shared_mem_size += std::mem::size_of::<SwkbdStatusData>();
}
if swkbd.save_state_flags & (1 << 1) != 0 {
swkbd.learning_offset = shared_mem_size as _;
shared_mem_size += std::mem::size_of::<SwkbdLearningData>();
}
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::<SwkbdState>(),
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::<SwkbdState>()
{
return;
}
let swkbd = unsafe { &mut *msg.cast::<SwkbdState>() };
let data = unsafe { *user.cast::<MessageCallbackData>() };
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::<SwkbdState>() as _,
0,
)
};
}
}
impl ParentalLock {

24
ctru-rs/src/console.rs

@ -5,8 +5,7 @@ @@ -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<S: Screen + Swap + Flush> ConsoleScreen for S {} @@ -63,7 +62,7 @@ impl<S: Screen + Swap + Flush> 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<PrintConsole>,
context: Box<UnsafeCell<PrintConsole>>,
screen: RefMut<'screen, dyn ConsoleScreen>,
}
@ -107,9 +106,9 @@ impl<'screen> Console<'screen> { @@ -107,9 +106,9 @@ impl<'screen> Console<'screen> {
/// ```
#[doc(alias = "consoleInit")]
pub fn new<S: ConsoleScreen>(screen: RefMut<'screen, S>) -> Self {
let mut context = Box::<PrintConsole>::default();
let context = Box::<UnsafeCell<PrintConsole>>::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> { @@ -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> { @@ -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> { @@ -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<'_> { @@ -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<'_> { @@ -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 {

1
ctru-rs/src/lib.rs

@ -18,6 +18,7 @@ @@ -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)]

4
ctru-rs/src/linear.rs

@ -42,6 +42,8 @@ unsafe impl Allocator for LinearAllocator { @@ -42,6 +42,8 @@ unsafe impl Allocator for LinearAllocator {
#[doc(alias = "linearFree")]
unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) {
ctru_sys::linearFree(ptr.as_ptr().cast());
unsafe {
ctru_sys::linearFree(ptr.as_ptr().cast());
}
}
}

5
ctru-rs/src/services/am.rs

@ -51,6 +51,11 @@ impl<'a> Title<'a> { @@ -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.

87
ctru-rs/src/services/apt.rs

@ -82,6 +82,48 @@ impl Apt { @@ -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 { @@ -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() }
}
}

2
ctru-rs/src/services/gfx.rs

@ -239,7 +239,7 @@ pub struct Gfx { @@ -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.

8
ctru-rs/src/services/ir_user.rs

@ -347,9 +347,11 @@ impl IrUser { @@ -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)
}
}
}

1
ctru-rs/src/services/ndsp/mod.rs

@ -21,7 +21,6 @@ use crate::error::ResultCode; @@ -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;

23
ctru-rs/src/services/svc.rs

@ -50,19 +50,28 @@ impl HandleExt for Handle { @@ -50,19 +50,28 @@ impl HandleExt for Handle {
) -> crate::Result<Vec<u32>> {
// 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)
}

2
ctru-rs/src/services/uds.rs

@ -128,7 +128,7 @@ impl TryFrom<u8> for ConnectionType { @@ -128,7 +128,7 @@ impl TryFrom<u8> for ConnectionType {
/// ID for a node on the network.
#[doc(alias = "NetworkNodeID")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeID {
/// No node ID set (not connected to a network).
None,

4
ctru-sys/build.rs

@ -73,12 +73,14 @@ fn main() { @@ -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)

18
ctru-sys/src/lib.rs

@ -16,6 +16,24 @@ @@ -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

Loading…
Cancel
Save