Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Get software keyboard input without max byte size parameter #157

Merged
merged 25 commits into from
Feb 20, 2024

Conversation

FenrirWolf
Copy link
Member

Ever since I first started messing with the software keyboard from Rust, I've always hated that libctru's swkbd API forces you to use a fixed-size buffer in order to receive string data from the keyboard. We've worked around that restriction by just forcing the user to specify a maximum size for the intermediate text buffer, but I've always hated that solution too. And so I randomly decided it's finally time to do something about it.

However, it turns out that "doing something about it" involves reimplementing both swkbdInputText and swkbdMessageCallback in Rust and handling a whole bunch of gnarly unsafe code ourselves instead of just leaving all of that nonsense to libctru.

Is it worth introducing this much unsafe code just to solve a small API nitpick? Frankly I'm not entirely sure, but I thought I'd throw this out there and see what you guys think.

@FenrirWolf FenrirWolf requested a review from a team as a code owner February 12, 2024 00:49
@FenrirWolf FenrirWolf force-pushed the software-keyboard-shenanigans branch 5 times, most recently from ab8d86d to 65f7d26 Compare February 14, 2024 06:48
@Meziu
Copy link
Member

Meziu commented Feb 14, 2024

Wow! This re-implementation looks quite nice. I must say I'm not against the "amounts" of unsafe code present here, since it manages to strengthen the API quite a lot and give us more control over the internal implementation (as I said in the past, especially regarding ndsp, there is a lot of things we could improve by getting our hands a bit dirty).

I'll try a deep dive review once I manage to get some time.

@FenrirWolf FenrirWolf force-pushed the software-keyboard-shenanigans branch 3 times, most recently from 890e226 to e593abd Compare February 14, 2024 23:33
@FenrirWolf
Copy link
Member Author

FenrirWolf commented Feb 16, 2024

Okay, I think this PR is in a pretty good state now and I probably won't need to push any more changes. I'm not sure what the nightly test failures are about though. They don't seem related to anything actually going on in the code here.

Copy link
Member

@ian-h-chamberlain ian-h-chamberlain left a comment

Choose a reason for hiding this comment

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

Mostly LGTM, had some small comments / questions about some of the implementation but a lot of it is just musing about future possibilities and I don't think anything blocking.

IMO probably would be fine to merge as-is with one more approval

struct MessageCallbackData {
extra: *mut SwkbdExtra,
swkbd_shared_mem_ptr: *mut libc::c_void,
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this something that we might eventually be able to generalize to a MessageCallbackData<T> where T = SwkbdExtra in this case? I know some people have been interested in general-purpose svc helpers to work with system calls that don't have "nice" APIs in libctru, so just wondering how generic this code is.

Definitely not asking for something like that in this PR, but just wondering if this PR might provide a good starting point from which that could be built. I believe some of the ir_user module by @AzureMarker also did a bit of svc memory allocation and stuff but I am not familiar with the specifics so not sure if there is any overlap there.

Copy link
Member Author

Choose a reason for hiding this comment

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

I only made this struct because libctru doesn't include a pointer to the shared memory block in SwkbdExtra like it ought to, and instead uses a static to pass the pointer into the APT message callback. I decided there's no reason we have to do the same in our implementation.

I've never messed with any other APT message calls, so I'm not sure if this specific struct would be helpful outside of this particular context.

Comment on lines 660 to 662
shared_mem_size +=
(std::mem::size_of::<u16>() * (swkbd.max_text_len as usize + 1) + 3) & !3;
Copy link
Member

Choose a reason for hiding this comment

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

I assume the + 3) & !3 is an alignment thing (align to usize, maybe)? does it make sense to have something like this just to make the math more obvious?

let max_len = swkbd.max_text_len as usize + 1;

shared_mem_size += (size_of::<u16>() * max_len).next_multiple_of(size_of::<usize>());

// etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that might make more sense. I'm honestly not very fluent in bitwise math, so I just kinda translated the C code directly without getting too fancy about interpreting its meaning.

Comment on lines +685 to +693
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>();
}
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice if we could use bitflags for this but I think this struct is defined by ctru_sys right? so maybe there's no great option but maybe defining consts for the bitshifts would be useful

shared_mem_size += std::mem::size_of::<SwkbdLearningData>();
}

shared_mem_size = (shared_mem_size + 0xFFF) & !0xFFF;
Copy link
Member

Choose a reason for hiding this comment

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

similar comment, could this be

swkbd.shared_memory_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) };
Copy link
Member

Choose a reason for hiding this comment

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

huh, I was thinking it would be nice to use Rust alloc for this but I guess it's passed to C immediately anyway so there's not really a compelling reason to do that that I can think of.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did think about using std::alloc, but as you said the memory block is only being manipulated by C code anyway and so it's more straightforward to just use memalign and free

ctru-rs/src/applets/swkbd.rs Outdated Show resolved Hide resolved

if swkbd.text_length > 0 {
let text16 = unsafe {
widestring::Utf16Str::from_slice_unchecked(std::slice::from_raw_parts(
Copy link
Member

Choose a reason for hiding this comment

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

I assume the _unchecked here means that we are trusting either the OS or libctru to give us a valid string, right? Just wondering if an extra check here might be worth a little bit of safety

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we're trusting the Software Keyboard to give us proper UTF-16 text here and I don't think there's any reason to suspect it would fail at that job

swkbd_shared_mem_ptr.add(swkbd.learning_offset as _).cast(),
extra.learning_data,
1,
)
Copy link
Member

Choose a reason for hiding this comment

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

nit: might increase readibility here to use the method form:

extra.learning_data.copy_from_nonoverlapping(
	swkbd_shared_mem_ptr.add(swkbd.learning_offset as _).cast(),
	1,
);

};
}

unsafe { libc::free(swkbd_shared_mem_ptr) };
Copy link
Member

Choose a reason for hiding this comment

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

Allocating and freeing all the options every time is probably fine, and I don't think we need to optimize right now — but I wonder if we could also have more of a Builder API pattern that sets all the sizing options up front and allocates just once, which would probably make this call cheaper.
Don't want to prematurely optimize though since swkbd isn't likely to be a hot path for most apps (I hope, lol!)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, calling the software keyboard involves a lot of setup no matter how you slice it. I'm not sure if it would be worth trying to reduce allocations here, especially since any changes to the software keyboard config would require recomputing and reallocating the memory block anyway.

// A reimplementation of `swkbdMessageCallback` from `libctru/source/applets/swkbd.c`.
// This is only needed because the original function is private to libctru, so we can't
// simply reuse their version
#[deny(unsafe_op_in_unsafe_fn)]
Copy link
Member

Choose a reason for hiding this comment

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

❤️❤️❤️

I have sometimes considered if we should consider denying as a library lint but I think it would be a nontrivial amount of work so never bothered to try

Copy link
Member

Choose a reason for hiding this comment

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

I am in favour of this as well! Unsafe blocks should be marked only when necessary and not all unsafe functions use unsafe code (especially around here...).

@FenrirWolf
Copy link
Member Author

Also while we're messing around in here, what do you guys think about renaming SoftwareKeyboard::get_string to SoftwareKeyboard::launch? I only chose the former name back when there were 2 functions for getting text data from the keyboard and I wanted to make sure there was a strong distinction between the two, but since this PR does away with the write_exact method, I feel like that would be a sensible change

@ian-h-chamberlain
Copy link
Member

Also while we're messing around in here, what do you guys think about renaming SoftwareKeyboard::get_string to SoftwareKeyboard::launch?

Works for me

Copy link
Member

@Meziu Meziu left a comment

Choose a reason for hiding this comment

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

LGTM. The only things I'd like to see improved are the alignment calculations that @ian-h-chamberlain pointed out. Other than that, it's fine.

@FenrirWolf
Copy link
Member Author

Apparently next_multiple_of is only stable as of rust 1.73 and nightly-2023-06-01 is older than that. Everything works on my end though.

@FenrirWolf FenrirWolf merged commit 0345f49 into rust3ds:master Feb 20, 2024
2 of 4 checks passed
@FenrirWolf FenrirWolf deleted the software-keyboard-shenanigans branch February 20, 2024 22:31
ian-h-chamberlain added a commit to rust3ds/actions that referenced this pull request Feb 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants