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

Implement String-like wrapper around FuriString #50

Merged
merged 9 commits into from
May 7, 2023

Conversation

str4d
Copy link
Contributor

@str4d str4d commented Mar 4, 2023

std::string::String methods to be implemented:

  • String::new() -> String
  • String::with_capacity(capacity: usize) -> String
  • String::from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
  • String::from_utf8_lossy(v: &[u8]) -> Cow<'_, str>
  • String::from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>
  • String::from_utf16_lossy(v: &[u16]) -> String
  • String::into_raw_parts(self) -> (*mut u8, usize, usize)
    • Doesn't make sense for FuriString. We could maybe expose *mut sys::FuriString?
  • String::from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String
    • We'd only expose this (via unsafe) if we want to leave *mut sys::FuriString exposed.
  • String::from_utf8_unchecked(bytes: Vec<u8>) -> String
    • Doesn't make sense for FuriString.
  • String::into_bytes(self) -> Vec<u8>
    • Doesn't make sense for FuriString (we aren't storing the string internally as a Vec<u8>, so it's better to just call String::to_bytes().to_owned()).
  • String::as_str(&self) -> &str
    • String::as_c_str(&self) -> &CStr
  • String::as_mut_str(&mut self) -> &mut str
  • String::push_str(&mut self, string: &str)
  • String::capacity(&self) -> usize
    • Flipper Zero SDK doesn't expose the necessary method.
  • String::reserve(&mut self, additional: usize)
  • String::reserve_exact(&mut self, additional: usize)
    • Flipper Zero SDK doesn't expose the necessary method.
  • String::try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError>
    • Flipper Zero SDK doesn't expose fallible FuriString allocations.
  • String::try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError>
    • Flipper Zero SDK doesn't expose fallible FuriString allocations.
  • String::shrink_to_fit(&mut self)
    • Flipper Zero SDK doesn't expose the necessary method.
  • String::shrink_to(&mut self, min_capacity: usize)
    • Flipper Zero SDK doesn't expose the necessary method.
  • String::push(&mut self, ch: char)
  • String::as_bytes(&self) -> &[u8]
    • Renamed to String::to_bytes because it's a bit more expensive.
    • Also added String::to_bytes_with_nul for parity with CString.
  • String::truncate(&mut self, new_len: usize)
  • String::pop(&mut self) -> Option<char>
  • String::remove(&mut self, idx: usize) -> char
  • String::retain<F>(&mut self, mut f: F) where F: FnMut(char) -> bool
  • String::insert(&mut self, idx: usize, ch: char)
  • String::insert_str(&mut self, idx: usize, string: &str)
  • String::as_mut_vec(&mut self) -> &mut Vec<u8>
    • Doesn't make sense for FuriString.
  • String::len(&self) -> usize
  • String::is_empty(&self) -> bool
  • String::split_off(&mut self, at: usize) -> String
  • String::clear(&mut self)
  • String::drain<R>(&mut self, range: R) -> Drain<'_> where R: RangeBounds<usize>
  • String::replace_range<R>(&mut self, range: R, replace_with: &str) where R: RangeBounds<usize>
  • String::into_boxed_str(self) -> Box<str>
    • Doesn't make sense for FuriString.
  • impl Clone for String
  • impl FromIterator<char> for String
  • impl<'a> FromIterator<&'a char> for String
  • impl<'a> FromIterator<&'a str> for String
  • impl FromIterator<String> for String
  • impl FromIterator<Box<str>> for String
  • impl<'a> FromIterator<Cow<'a, str>> for String
  • impl Extend<char> for String
  • impl<'a> Extend<&'a char> for String
  • impl<'a> Extend<&'a str> for String
  • impl Extend<Box<str>> for String
  • impl Extend<String> for String
  • impl<'a> Extend<Cow<'a, str>> for String
  • impl PartialEq for String
  • impl Eq for String
  • impl PartialOrd for String
  • impl Ord for String
  • impl Default for String
  • impl fmt::Display for String
  • impl fmt::Debug for String
  • impl hash::Hash for String
  • impl Add<&str> for String
  • impl AddAssign<&str> for String
  • impl ops::Index<ops::Range<usize>> for String
  • impl ops::Index<ops::RangeTo<usize>> for String
  • impl ops::Index<ops::RangeFrom<usize>> for String
  • impl ops::Index<ops::RangeFull> for String
  • impl ops::Index<ops::RangeInclusive<usize>> for String
  • impl ops::Index<ops::RangeToInclusive<usize>> for String
  • impl ops::IndexMut<ops::Range<usize>> for String
  • impl ops::IndexMut<ops::RangeTo<usize>> for String
  • impl ops::IndexMut<ops::RangeFrom<usize>> for String
  • impl ops::IndexMut<ops::RangeFull> for String
  • impl ops::IndexMut<ops::RangeInclusive<usize>> for String
  • impl ops::IndexMut<ops::RangeToInclusive<usize>> for String
  • impl ops::Deref for String
  • impl ops::DerefMut for String
  • impl FromStr for String
  • impl AsRef<str> for String
    • impl AsRef<CStr> for String
  • impl AsMut<str> for String
  • impl AsRef<[u8]> for String
  • impl From<&str> for String
  • impl From<&mut str> for String
  • impl From<&String> for String
  • impl From<Box<str>> for String
  • impl From<String> for Box<str>
    • Doesn't make sense for FuriString.
  • impl<'a> From<Cow<'a, str>> for String
  • impl<'a> From<String> for Cow<'a, str>
    • Doesn't make sense for FuriString.
  • impl<'a> From<&'a String> for Cow<'a, str>
    • Doesn't make sense for FuriString.
  • impl<'a> FromIterator<String> for Cow<'a, str>
    • Doesn't make sense for FuriString.
  • impl From<String> for Vec<u8>
    • Doesn't make sense for FuriString.
  • impl fmt::Write for String
  • impl From<char> for String

str methods to be implemented:

  • str::len(&self) -> usize (see String)
  • str::is_empty(&self) -> bool (see String)
  • str::is_char_boundary(&self, index: usize) -> bool
  • str::floor_char_boundary(&self, index: usize) -> usize
  • str::ceil_char_boundary(&self, index: usize) -> usize
  • str::as_bytes(&self) -> &[u8] (see String)
  • str::as_bytes_mut(&mut self) -> &mut [u8]
  • str::as_ptr(&self) -> *const u8
  • str::as_mut_ptr(&mut self) -> *mut u8
  • str::get<I: ~const SliceIndex<str>>(&self, i: I) -> Option<&I::Output>
  • str::get_mut<I: ~const SliceIndex<str>>(&mut self, i: I) -> Option<&mut I::Output>
  • str::get_unchecked<I: ~const SliceIndex<str>>(&self, i: I) -> &I::Output
  • str::get_unchecked_mut<I: ~const SliceIndex<str>>(&mut self, i: I) -> &mut I::Output
  • str::slice_unchecked(&self, begin: usize, end: usize) -> &str (deprecated)
  • str::slice_mut_unchecked(&mut self, begin: usize, end: usize) -> &mut str (deprecated)
  • str::split_at(&self, mid: usize) -> (&str, &str)
  • str::split_at_mut(&mut self, mid: usize) -> (&mut str, &mut str)
  • str::chars(&self) -> Chars<'_>
    • Renamed to FuriString::chars_lossy because it inserts replacement characters.
  • str::char_indices(&self) -> CharIndices<'_>
    • Renamed to FuriString::chars_lossy because it inserts replacement characters.
  • str::bytes(&self) -> Bytes<'_>
  • str::split_whitespace(&self) -> SplitWhitespace<'_>
  • str::split_ascii_whitespace(&self) -> SplitAsciiWhitespace<'_>
  • str::lines(&self) -> Lines<'_>
  • str::lines_any(&self) -> LinesAny<'_> (deprecated)
  • str::encode_utf16(&self) -> EncodeUtf16<'_>
  • str::contains<'a, P: Pattern<'a>>(&'a self, pat: P) -> bool
  • str::starts_with<'a, P: Pattern<'a>>(&'a self, pat: P) -> bool
  • str::ends_with<'a, P>(&'a self, pat: P) -> bool
  • str::find<'a, P: Pattern<'a>>(&'a self, pat: P) -> Option<usize>
  • str::rfind<'a, P>(&'a self, pat: P) -> Option<usize>
  • str::split<'a, P: Pattern<'a>>(&'a self, pat: P) -> Split<'a, P>
  • str::split_inclusive<'a, P: Pattern<'a>>(&'a self, pat: P) -> SplitInclusive<'a, P>
  • str::rsplit<'a, P>(&'a self, pat: P) -> RSplit<'a, P>
  • str::split_terminator<'a, P: Pattern<'a>>(&'a self, pat: P) -> SplitTerminator<'a, P>
  • str::rsplit_terminator<'a, P>(&'a self, pat: P) -> RSplitTerminator<'a, P>
  • str::splitn<'a, P: Pattern<'a>>(&'a self, n: usize, pat: P) -> SplitN<'a, P>
  • str::rsplitn<'a, P>(&'a self, n: usize, pat: P) -> RSplitN<'a, P>
  • str::split_once<'a, P: Pattern<'a>>(&'a self, delimiter: P) -> Option<(&'a str, &'a str)>
  • str::rsplit_once<'a, P>(&'a self, delimiter: P) -> Option<(&'a str, &'a str)>
  • str::matches<'a, P: Pattern<'a>>(&'a self, pat: P) -> Matches<'a, P>
  • str::rmatches<'a, P>(&'a self, pat: P) -> RMatches<'a, P>
  • str::match_indices<'a, P: Pattern<'a>>(&'a self, pat: P) -> MatchIndices<'a, P>
  • str::rmatch_indices<'a, P>(&'a self, pat: P) -> RMatchIndices<'a, P>
  • str::trim(&self) -> &str
    • Made mutable.
  • str::trim_start(&self) -> &str
    • Made mutable.
  • str::trim_end(&self) -> &str
    • Made mutable.
  • str::trim_left(&self) -> &str (deprecated)
  • str::trim_right(&self) -> &str (deprecated)
  • str::trim_matches<'a, P>(&'a self, pat: P) -> &'a str
    • Made mutable.
  • str::trim_start_matches<'a, P: Pattern<'a>>(&'a self, pat: P) -> &'a str
    • Made mutable.
  • str::strip_prefix<'a, P: Pattern<'a>>(&'a self, prefix: P) -> Option<&'a str>
    • Made mutable and returns bool.
  • str::strip_suffix<'a, P: Pattern<'a>>(&'a self, suffix: P) -> Option<&'a str>
    • Made mutable and returns bool.
  • str::trim_end_matches<'a, P>(&'a self, pat: P) -> &'a str
    • Made mutable.
  • str::trim_left_matches<'a, P: Pattern<'a>>(&'a self, pat: P) -> &'a str (deprecated)
  • str::trim_right_matches<'a, P>(&'a self, pat: P) -> &'a str (deprecated)
  • str::parse<F: FromStr>(&self) -> Result<F, F::Err>
  • str::is_ascii(&self) -> bool
  • str::eq_ignore_ascii_case(&self, other: &str) -> bool
  • str::make_ascii_uppercase(&mut self)
  • str::make_ascii_lowercase(&mut self)
  • str::escape_debug(&self) -> EscapeDebug<'_>
  • str::escape_default(&self) -> EscapeDefault<'_>
  • str::escape_unicode(&self) -> EscapeUnicode<'_>

@str4d str4d mentioned this pull request Mar 4, 2023
@str4d str4d force-pushed the string branch 3 times, most recently from d291a24 to b726e0e Compare March 4, 2023 01:30
@str4d str4d changed the title Implement std::string::String-like wrapper around FuriString Implement String-like and str-like wrappers around FuriString Mar 4, 2023
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
@str4d str4d force-pushed the string branch 2 times, most recently from 8644efb to 8abae06 Compare March 5, 2023 01:06
@str4d str4d changed the title Implement String-like and str-like wrappers around FuriString Implement String-like wrapper around FuriString Mar 5, 2023
@str4d
Copy link
Contributor Author

str4d commented Mar 12, 2023

The PR currently focuses on providing newly-constructed, Rust-owned FuriStrings. But we will also want the ability to represent FuriStrings inside types owned by the Flipper Zero SDK (which we cannot free in a Rust Drop impl). I think the way to handle that is to add a constructor like:

impl String {
    pub(crate) unsafe fn from_borrowed_mut<'s>(raw: *mut FuriString) -> &'s mut String {
        todo!("pointer magic")
    }
}

which is only used from inside other wrapper types, enabling them to implement APIs like:

pub struct OtherWrapper(*mut sys::SomeType);

impl OtherWrapper {
    pub fn name_mut(&mut self) -> &mut furi::String {
        unsafe { furi::String::from_borrowed_mut((*self.0).name) }
    }
}

@str4d
Copy link
Contributor Author

str4d commented Apr 25, 2023

Rebased on main to bring in the test harness.

@str4d
Copy link
Contributor Author

str4d commented Apr 25, 2023

Pushed a test suite (adapted from https://github.com/rust-lang/rust/blob/master/library/alloc/tests/string.rs). The uncommented tests are the ones for APIs I have currently implemented. The tests do all pass, but I've seen a few occasional panics that I haven't been able to pin down yet.

@str4d
Copy link
Contributor Author

str4d commented Apr 25, 2023

Force-pushed to separate the upstream tests from my changes.

@str4d
Copy link
Contributor Author

str4d commented Apr 26, 2023

Force-pushed to add some more trait impls.

@str4d
Copy link
Contributor Author

str4d commented Apr 26, 2023

Almost all the remaining APIs of std::string::String that I haven't implemented here yet depend in some way on whether we want flipperzero::furi::string::String to be:

  • a CString with better APIs?
  • a String that allows non-UTF-8?
  • a String that should enforce UTF-8 (and thus enable &str to be used for slicing) but happens to have a trailing nul in its internal representation?

In particular, I'm trying to decide whether to provide a String::chars method. Rust's char type is defined as a Unicode scalar value, so while we can easily have String::push(char), we cannot iterate over chars unless String is guaranteed to contain valid UTF-8. But most of the remaining APIs (pop, remove, retain, etc.) are implemented in terms of String::chars. So we either don't provide them, we restrict String to be UTF-8, or we provide them but in terms of bytes.

@dcoles
Copy link
Collaborator

dcoles commented Apr 27, 2023

It's been a while since I've thought about this, but I think my direction of thinking was:

  • Any flipperzero::furi::string::String must be trivially be converted into a sys::FuriString (and vice-versa)
    • This is to allow interacting with the FlipperZero API without needing additional allocations in either direction
  • As such, flipperzero::furi::string::String is guaranteed to be \0 terminated
  • As such, it can implement Borrow/AsRef<CStr> but can't be trivially sliced
  • We can support things like trim or mid but they act in-place using the furi_string_ methods
  • We can support to_bytes/to_bytes_with_null to get &[u8] slices
  • We can support to_str to get a Result<&str, Utf8Error>
  • Where possible, you should still be able to use string literals (treated as utf-8 bytes)
    • e.g. push(char) or push_str(&str) are easy to implement
  • It should be fine to allow accessing as a &mut [u8] as long as the slice excludes the final \0

I suspect we could even support an iterator over unicode scalars built on furi_string_utf8_decode that returned REPLACEMENT_CHARACTER or None whenever it encountered an invalid UTF-8 sequence, but I think we'd quickly see diminishing returns in respect to implementation effort to support the remaining methods that operate on char. At that point, it's probably best to just use a Rust String and convert it to flipperzero::String when you're done.

@str4d
Copy link
Contributor Author

str4d commented May 6, 2023

Rebased on main to fix merge conflict in flipperzero::furi.

str4d added 3 commits May 6, 2023 02:40
- Source: https://github.com/rust-lang/rust
- Revision: 7b58193f90185a5730732a727362576a9bdca26b
- Path: rust/library/alloc/tests/string.rs
- License: MIT
Also removes the tests that are not relevant to `FuriString`.
@str4d
Copy link
Contributor Author

str4d commented May 6, 2023

Force-pushed to rename String::push_rust_str back to String::push_str as the PR no longer tries to provide its own str type.

str4d added 3 commits May 6, 2023 17:04
- Source: https://github.com/rust-lang/rust
- Revision: 7b58193f90185a5730732a727362576a9bdca26b
- Path: rust/library/alloc/tests/str.rs
- License: MIT
Also removes the tests that are not relevant to `FuriString`.
@str4d
Copy link
Contributor Author

str4d commented May 6, 2023

I've implemented a bunch of the pattern-based str methods now, as well as importing the corresponding test suite from rust-lang/rust.

At this point I suspect we are approaching diminishing returns. I can continue to slowly implement additional methods and get more of the tests uncommented and passing, but that doesn't necessarily have to block this PR. I'm going to pause that work and ensure that I can use furi::String in #47.

@str4d str4d marked this pull request as ready for review May 6, 2023 17:53
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
crates/flipperzero/src/furi/string.rs Outdated Show resolved Hide resolved
@dcoles
Copy link
Collaborator

dcoles commented May 7, 2023

Looking great! My vote is to get the current work merged in (it's already far superior to CString) and add any additional functionality on a case-by-case basis.

@str4d
Copy link
Contributor Author

str4d commented May 7, 2023

Okay, I think that's everything 🙂

This enables us to improve the `Debug` and `Display` impls.
@str4d
Copy link
Contributor Author

str4d commented May 7, 2023

Okay, having committed the new file, now that's everything 😂

@dcoles dcoles merged commit 917cf81 into flipperzero-rs:main May 7, 2023
@dcoles
Copy link
Collaborator

dcoles commented May 7, 2023

Thanks for the PR! This one was big one and will be extremely useful!

@dcoles
Copy link
Collaborator

dcoles commented May 8, 2023

@str4d Since you've by far the most active contributor to this repository, I've invited you to @flipperzero-rs/maintainers. This should grant you fairly broad permission to push, merge, etc. No obligation to accept, but it would be good to have another pair of eyes watching things since I'm not always as active as I would like.

@str4d str4d deleted the string branch May 8, 2023 00:35
@str4d str4d added this to the v0.9.0 milestone May 10, 2023
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.

2 participants