Skip to content

Commit efaf576

Browse files
committed
Improve NSString
With ideas from https://github.com/nvzqz/fruity
1 parent a564776 commit efaf576

File tree

2 files changed

+208
-3
lines changed

2 files changed

+208
-3
lines changed

objc2-foundation/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1111
`NSArray`, which derefs to `NSObject`, which derefs to `Object`.
1212

1313
This allows more ergonomic usage.
14+
* Implement `PartialOrd` and `Ord` for `NSString`.
15+
* Added `NSString::has_prefix` and `NSString::has_suffix`.
1416

1517
### Changed
1618
* **BREAKING**: Removed the following helper traits in favor of inherent

objc2-foundation/src/string.rs

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::cmp;
12
use core::ffi::c_void;
23
use core::fmt;
34
use core::ptr::NonNull;
@@ -11,8 +12,9 @@ use objc2::msg_send;
1112
use objc2::rc::DefaultId;
1213
use objc2::rc::{autoreleasepool, AutoreleasePool};
1314
use objc2::rc::{Id, Shared};
15+
use objc2::runtime::Bool;
1416

15-
use super::{NSCopying, NSObject};
17+
use crate::{NSComparisonResult, NSCopying, NSObject};
1618

1719
#[cfg(apple)]
1820
const UTF8_ENCODING: usize = 4;
@@ -24,25 +26,101 @@ const UTF8_ENCODING: i32 = 4;
2426
const NSNotFound: ffi::NSInteger = ffi::NSIntegerMax;
2527

2628
object! {
29+
/// A static, plain-text Unicode string object.
30+
///
31+
/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring?language=objc).
2732
unsafe pub struct NSString: NSObject;
33+
// TODO: Use isEqualToString: for comparison (instead of just isEqual:)
34+
// The former is more performant
35+
36+
// TODO: Use more performant Debug implementation.
37+
38+
// TODO: Check if performance of NSSelectorFromString is worthwhile
2839
}
2940

3041
// TODO: SAFETY
3142
unsafe impl Sync for NSString {}
3243
unsafe impl Send for NSString {}
3344

3445
impl NSString {
35-
unsafe_def_fn!(pub fn new -> Shared);
46+
unsafe_def_fn! {
47+
/// Construct an empty NSString.
48+
pub fn new -> Shared;
49+
}
3650

51+
/// The number of UTF-8 code units in `self`.
52+
#[doc(alias = "lengthOfBytesUsingEncoding")]
53+
#[doc(alias = "lengthOfBytesUsingEncoding:")]
3754
pub fn len(&self) -> usize {
3855
unsafe { msg_send![self, lengthOfBytesUsingEncoding: UTF8_ENCODING] }
3956
}
4057

58+
/// The number of UTF-16 code units in `self`.
59+
///
60+
/// See also [`NSString::len`].
61+
#[doc(alias = "length")]
62+
// TODO: Finish this
63+
fn len_utf16(&self) -> usize {
64+
unsafe { msg_send![self, lengt] }
65+
}
66+
4167
pub fn is_empty(&self) -> bool {
68+
// TODO: lengthOfBytesUsingEncoding: might sometimes return 0 for
69+
// other reasons, so this is not really correct!
4270
self.len() == 0
4371
}
4472

45-
/// TODO
73+
/// Get the [`str`](`prim@str`) representation of this string if it can be
74+
/// done efficiently.
75+
///
76+
/// Returns [`None`] if the internal storage does not allow this to be
77+
/// done efficiently. Use [`NSString::to_str`] or [`NSString::to_string`]
78+
/// if performance is not an issue.
79+
#[doc(alias = "CFStringGetCStringPtr")]
80+
#[allow(unused)]
81+
// TODO: Finish this
82+
fn as_str_wip(&self) -> Option<&str> {
83+
type CFStringEncoding = u32;
84+
#[allow(non_upper_case_globals)]
85+
// https://developer.apple.com/documentation/corefoundation/cfstringbuiltinencodings/kcfstringencodingutf8?language=objc
86+
const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
87+
extern "C" {
88+
// https://developer.apple.com/documentation/corefoundation/1542133-cfstringgetcstringptr?language=objc
89+
fn CFStringGetCStringPtr(s: &NSString, encoding: CFStringEncoding) -> *const c_char;
90+
}
91+
let bytes = unsafe { CFStringGetCStringPtr(self, kCFStringEncodingUTF8) };
92+
NonNull::new(bytes as *mut u8).map(|bytes| {
93+
let len = self.len();
94+
let bytes: &[u8] = unsafe { slice::from_raw_parts(bytes.as_ptr(), len) };
95+
str::from_utf8(bytes).unwrap()
96+
})
97+
}
98+
99+
/// Get an [UTF-16] string slice if it can be done efficiently.
100+
///
101+
/// Returns [`None`] if the internal storage of `self` does not allow this
102+
/// to be returned efficiently.
103+
///
104+
/// See [`as_str`](Self::as_str) for the UTF-8 equivalent.
105+
///
106+
/// [UTF-16]: https://en.wikipedia.org/wiki/UTF-16
107+
#[allow(unused)]
108+
// TODO: Finish this
109+
fn as_utf16(&self) -> Option<&[u16]> {
110+
extern "C" {
111+
// https://developer.apple.com/documentation/corefoundation/1542939-cfstringgetcharactersptr?language=objc
112+
fn CFStringGetCharactersPtr(s: &NSString) -> *const u16;
113+
}
114+
let ptr = unsafe { CFStringGetCharactersPtr(self) };
115+
NonNull::new(ptr as *mut u16)
116+
.map(|ptr| unsafe { slice::from_raw_parts(ptr.as_ptr(), self.len_utf16()) })
117+
}
118+
119+
/// Get the [`str`](`prim@str`) representation of this.
120+
///
121+
/// TODO: Further explain this.
122+
///
123+
/// # Examples
46124
///
47125
/// ```compile_fail
48126
/// # use objc2::rc::autoreleasepool;
@@ -61,6 +139,7 @@ impl NSString {
61139
/// let ns_string = NSString::new();
62140
/// let s = autoreleasepool(|pool| ns_string.as_str(pool));
63141
/// ```
142+
#[doc(alias = "UTF8String")]
64143
pub fn as_str<'r, 's: 'r, 'p: 'r>(&'s self, pool: &'p AutoreleasePool) -> &'r str {
65144
// This is necessary until `auto` types stabilizes.
66145
pool.__verify_is_inner();
@@ -78,6 +157,8 @@ impl NSString {
78157
//
79158
// So the lifetime of the returned pointer is either the same as the
80159
// NSString OR the lifetime of the innermost @autoreleasepool.
160+
//
161+
// https://developer.apple.com/documentation/foundation/nsstring/1411189-utf8string?language=objc
81162
let bytes: *const c_char = unsafe { msg_send![self, UTF8String] };
82163
let bytes = bytes as *const u8;
83164
let len = self.len();
@@ -96,6 +177,11 @@ impl NSString {
96177
str::from_utf8(bytes).unwrap()
97178
}
98179

180+
// TODO: Allow usecases where the NUL byte from `UTF8String` is kept?
181+
182+
/// Creates an immutable `NSString` by copying the given string slice.
183+
#[doc(alias = "initWithBytes")]
184+
#[doc(alias = "initWithBytes:length:encoding:")]
99185
pub fn from_str(string: &str) -> Id<Self, Shared> {
100186
let cls = Self::class();
101187
let bytes = string.as_ptr() as *const c_void;
@@ -110,8 +196,64 @@ impl NSString {
110196
Id::new(NonNull::new_unchecked(obj))
111197
}
112198
}
199+
200+
// TODO: initWithBytesNoCopy:, maybe add lifetime parameter to NSString?
201+
// See https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/src/foundation/ns_string/mod.rs#L350-L381
202+
// Might be quite difficult, as Objective-C code might assume the NSString
203+
// is always alive?
204+
// See https://github.com/drewcrawford/foundationr/blob/b27683417a35510e8e5d78a821f081905b803de6/src/nsstring.rs
205+
206+
/// Whether the given string matches the beginning characters of this
207+
/// string.
208+
///
209+
/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring/1410309-hasprefix?language=objc).
210+
#[doc(alias = "hasPrefix")]
211+
#[doc(alias = "hasPrefix:")]
212+
pub fn has_prefix(&self, prefix: &NSString) -> bool {
213+
let res: Bool = unsafe { msg_send![self, hasPrefix: prefix] };
214+
res.is_true()
215+
}
216+
217+
/// Whether the given string matches the ending characters of this string.
218+
///
219+
/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring/1416529-hassuffix?language=objc).
220+
#[doc(alias = "hasSuffix")]
221+
#[doc(alias = "hasSuffix:")]
222+
pub fn has_suffix(&self, suffix: &NSString) -> bool {
223+
let res: Bool = unsafe { msg_send![self, hasSuffix: suffix] };
224+
res.is_true()
225+
}
226+
227+
// pub fn from_nsrange(range: NSRange) -> Id<Self, Shared>
228+
// https://developer.apple.com/documentation/foundation/1415155-nsstringfromrange?language=objc
229+
}
230+
231+
impl PartialOrd for NSString {
232+
#[inline]
233+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
234+
Some(self.cmp(other))
235+
}
236+
}
237+
238+
impl Ord for NSString {
239+
fn cmp(&self, other: &Self) -> cmp::Ordering {
240+
let res: NSComparisonResult = unsafe { msg_send![self, compare: other] };
241+
// TODO: Other comparison methods:
242+
// - compare:options:
243+
// - compare:options:range:
244+
// - compare:options:range:locale:
245+
// - localizedCompare:
246+
// - caseInsensitiveCompare:
247+
// - localizedCaseInsensitiveCompare:
248+
// - localizedStandardCompare:
249+
res.into()
250+
}
113251
}
114252

253+
// TODO: PartialEq and PartialOrd against &str
254+
// See `fruity`'s implementation:
255+
// https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/src/foundation/ns_string/mod.rs#L69-L163
256+
115257
impl DefaultId for NSString {
116258
type Ownership = Shared;
117259

@@ -192,6 +334,16 @@ mod tests {
192334
});
193335
}
194336

337+
#[test]
338+
fn test_nul() {
339+
let expected = "\0";
340+
let s = NSString::from_str(expected);
341+
assert_eq!(s.len(), expected.len());
342+
autoreleasepool(|pool| {
343+
assert_eq!(s.as_str(pool), expected);
344+
});
345+
}
346+
195347
#[test]
196348
fn test_interior_nul() {
197349
let expected = "Hello\0World";
@@ -246,4 +398,55 @@ mod tests {
246398
});
247399
assert_eq!(ns_string.len(), expected.len());
248400
}
401+
402+
#[test]
403+
fn test_hash() {
404+
use std::collections::hash_map::DefaultHasher;
405+
use std::hash::Hash;
406+
407+
let s1 = NSString::from_str("example string goes here");
408+
let s2 = NSString::from_str("example string goes here");
409+
410+
let mut hashstate = DefaultHasher::new();
411+
let mut hashstate2 = DefaultHasher::new();
412+
assert_eq!(s1.hash(&mut hashstate), s2.hash(&mut hashstate2));
413+
}
414+
415+
#[test]
416+
fn test_prefix_suffix() {
417+
let s = NSString::from_str("abcdef");
418+
let prefix = NSString::from_str("abc");
419+
let suffix = NSString::from_str("def");
420+
assert!(s.has_prefix(&prefix));
421+
assert!(s.has_suffix(&suffix));
422+
assert!(!s.has_prefix(&suffix));
423+
assert!(!s.has_suffix(&prefix));
424+
}
425+
426+
#[test]
427+
fn test_cmp() {
428+
let s1 = NSString::from_str("aa");
429+
assert!(s1 <= s1);
430+
assert!(s1 >= s1);
431+
let s2 = NSString::from_str("ab");
432+
assert!(s1 < s2);
433+
assert!(!(s1 > s2));
434+
assert!(s1 <= s2);
435+
assert!(!(s1 >= s2));
436+
let s3 = NSString::from_str("ba");
437+
assert!(s1 < s3);
438+
assert!(!(s1 > s3));
439+
assert!(s1 <= s3);
440+
assert!(!(s1 >= s3));
441+
assert!(s2 < s3);
442+
assert!(!(s2 > s3));
443+
assert!(s2 <= s3);
444+
assert!(!(s2 >= s3));
445+
446+
let s = NSString::from_str("abc");
447+
let shorter = NSString::from_str("a");
448+
let longer = NSString::from_str("abcdef");
449+
assert!(s > shorter);
450+
assert!(s < longer);
451+
}
249452
}

0 commit comments

Comments
 (0)