Browse Source

Merge pull request #157 from FenrirWolf/software-keyboard-shenanigans

Get software keyboard input without max byte size parameter
testing/vulkan-ci
FenrirWolf 10 months ago committed by GitHub
parent
commit
0345f49b8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/ci.yml
  2. 3
      ctru-rs/Cargo.toml
  3. 2
      ctru-rs/examples/file-explorer.rs
  4. 4
      ctru-rs/examples/software-keyboard.rs
  5. 377
      ctru-rs/src/applets/swkbd.rs

4
.github/workflows/ci.yml

@ -15,7 +15,7 @@ jobs:
matrix: matrix:
toolchain: toolchain:
# Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date # 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 # Check for breakage on latest nightly
- nightly - nightly
@ -53,7 +53,7 @@ jobs:
strategy: strategy:
matrix: matrix:
toolchain: toolchain:
- nightly-2023-06-01 - nightly-2024-02-18
- nightly - nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }} continue-on-error: ${{ matrix.toolchain == 'nightly' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest

3
ctru-rs/Cargo.toml

@ -10,7 +10,7 @@ categories = ["os", "api-bindings", "hardware-support"]
exclude = ["examples"] exclude = ["examples"]
license = "Zlib" license = "Zlib"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.73"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
@ -24,6 +24,7 @@ shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" }
pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" }
libc = "0.2.121" libc = "0.2.121"
bitflags = "2.3.3" bitflags = "2.3.3"
widestring = "1.0.2"
[build-dependencies] [build-dependencies]
toml = "0.5" toml = "0.5"

2
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)) { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) {
let mut keyboard = SoftwareKeyboard::default(); 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)) => { Ok((path, Button::Right)) => {
// Clicked "OK". // Clicked "OK".
action(self, path); action(self, path);

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

@ -44,9 +44,9 @@ fn main() {
// Check if the user request to write some input. // Check if the user request to write some input.
if hid.keys_down().contains(KeyPad::A) { 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. // 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((text, Button::Right)) => println!("You entered: {text}"),
Ok((_, Button::Left)) => println!("Cancelled"), Ok((_, Button::Left)) => println!("Cancelled"),
Ok((_, Button::Middle)) => println!("How did you even press this?"), Ok((_, Button::Middle)) => println!("How did you even press this?"),

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

@ -4,10 +4,14 @@
#![doc(alias = "keyboard")] #![doc(alias = "keyboard")]
use crate::services::{apt::Apt, gfx::Gfx}; use crate::services::{apt::Apt, gfx::Gfx};
use ctru_sys::{self, SwkbdState}; use ctru_sys::{
aptLaunchLibraryApplet, aptSetMessageCallback, envGetAptAppId, svcCloseHandle,
svcCreateMemoryBlock, APT_SendParameter, SwkbdButton, SwkbdDictWord, SwkbdExtra,
SwkbdLearningData, SwkbdState, SwkbdStatusData, APPID_SOFTWARE_KEYBOARD, APTCMD_MESSAGE,
NS_APPID,
};
use bitflags::bitflags; use bitflags::bitflags;
use libc;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::fmt::Display; use std::fmt::Display;
@ -110,7 +114,7 @@ pub enum ButtonConfig {
LeftMiddleRight = 3, LeftMiddleRight = 3,
} }
/// Error returned by an unsuccessful [`SoftwareKeyboard::get_string()`]. /// Error returned by an unsuccessful [`SoftwareKeyboard::launch()`].
#[doc(alias = "SwkbdResult")] #[doc(alias = "SwkbdResult")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(i32)] #[repr(i32)]
@ -207,6 +211,13 @@ bitflags! {
} }
} }
// Internal book-keeping struct used to send data to `aptSetMessageCallback` when calling the software keyboard.
// We only need this because libctru doesn't keep a pointer to the shared memory block in `SwkbdExtra` for whatever reason
struct MessageCallbackData {
extra: *mut SwkbdExtra,
swkbd_shared_mem_ptr: *mut libc::c_void,
}
impl SoftwareKeyboard { 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). /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3).
/// ///
@ -241,11 +252,6 @@ impl SoftwareKeyboard {
/// Launches the applet based on the given configuration and returns a string containing the text input. /// 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 /// # Example
/// ///
/// ``` /// ```
@ -260,67 +266,15 @@ impl SoftwareKeyboard {
/// use ctru::applets::swkbd::SoftwareKeyboard; /// use ctru::applets::swkbd::SoftwareKeyboard;
/// let mut keyboard = SoftwareKeyboard::default(); /// let mut keyboard = SoftwareKeyboard::default();
/// ///
/// let (text, button) = keyboard.get_string(2048, &apt, &gfx)?; /// let (text, button) = keyboard.launch(&apt, &gfx)?;
/// # /// #
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[doc(alias = "swkbdInputText")] #[doc(alias = "swkbdInputText")]
pub fn get_string( pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(String, Button), Error> {
&mut self, let mut output = String::new();
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
///
/// ```
/// # 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 mut buffer = vec![0; 100];
///
/// let button = keyboard.write_exact(&mut buffer, &apt, &gfx)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "swkbdInputText")]
pub fn write_exact(&mut self, buf: &mut [u8], _apt: &Apt, _gfx: &Gfx) -> Result<Button, Error> {
unsafe { unsafe {
// The filter callback gets reset every time the SoftwareKeyboard is used. // The filter callback gets reset every time the SoftwareKeyboard is used.
ctru_sys::swkbdSetFilterCallback( ctru_sys::swkbdSetFilterCallback(
@ -329,11 +283,11 @@ impl SoftwareKeyboard {
(self as *mut Self).cast(), (self as *mut Self).cast(),
); );
match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { match self.swkbd_input_text(&mut output) {
ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()),
ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), ctru_sys::SWKBD_BUTTON_LEFT => Ok((output, Button::Left)),
ctru_sys::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), ctru_sys::SWKBD_BUTTON_MIDDLE => Ok((output, Button::Middle)),
ctru_sys::SWKBD_BUTTON_RIGHT => Ok(Button::Right), ctru_sys::SWKBD_BUTTON_RIGHT => Ok((output, Button::Right)),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -654,7 +608,7 @@ impl SoftwareKeyboard {
/// ///
/// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, /// 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 /// 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 /// # Example
/// ///
@ -675,6 +629,293 @@ impl SoftwareKeyboard {
// Activate the specific validation rule for maximum length. // Activate the specific validation rule for maximum length.
self.state.valid_input = ValidInput::FixedLen.into(); self.state.valid_input = ValidInput::FixedLen.into();
} }
// A reimplementation of `swkbdInputText` from `libctru/source/applets/swkbd.c`. Allows us to
// get text from the software keyboard and put it directly into a `String` without requiring
// an intermediate fixed-size buffer
fn swkbd_input_text(&mut self, output: &mut String) -> 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 mut 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 !extra.initial_text.is_null() {
swkbd.initial_text_offset = 0;
unsafe {
let utf16_iter =
str::from_utf8_unchecked(CStr::from_ptr(extra.initial_text).to_bytes())
.encode_utf16()
.take(swkbd.max_text_len as _)
.chain(once(0));
let mut initial_text_cursor = swkbd_shared_mem_ptr.cast();
for code_unit in utf16_iter {
*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 extra.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);
let mut callback_data = MessageCallbackData {
extra: std::ptr::addr_of_mut!(extra),
swkbd_shared_mem_ptr,
};
if extra.callback.is_some() {
aptSetMessageCallback(
Some(Self::swkbd_message_callback),
std::ptr::addr_of_mut!(callback_data).cast(),
);
}
aptLaunchLibraryApplet(
APPID_SOFTWARE_KEYBOARD,
(swkbd as *mut SwkbdState).cast(),
std::mem::size_of::<SwkbdState>(),
swkbd_shared_mem_handle,
);
if extra.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 callback set by `swkbdSetFilterCallback`
#[deny(unsafe_op_in_unsafe_fn)]
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,
) {
let data = unsafe { &mut *user.cast::<MessageCallbackData>() };
let swkbd = unsafe { &mut *msg.cast::<SwkbdState>() };
let extra = unsafe { &mut *data.extra };
if sender != ctru_sys::APPID_SOFTWARE_KEYBOARD
|| msg_size != std::mem::size_of::<SwkbdState>()
{
return;
}
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 usize + 1,
))
};
let text8 = text16.to_string();
let mut retmsg = std::ptr::null();
if let Some(cb) = extra.callback {
swkbd.callback_result = unsafe {
cb(
extra.callback_user,
&mut retmsg,
text8.as_ptr(),
text8.len(),
)
} as _
};
let retmsg = if !retmsg.is_null() {
unsafe {
let len = libc::strlen(retmsg) + 1;
std::str::from_utf8_unchecked(std::slice::from_raw_parts(retmsg, len))
}
} else {
"\0"
};
let callback_msg = &mut swkbd.callback_msg;
for (idx, code_unit) in retmsg
.encode_utf16()
.take(callback_msg.len() - 1)
.enumerate()
{
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 { impl ParentalLock {

Loading…
Cancel
Save