Skip to content

Commit 2bd99a8

Browse files
authored
Merge pull request #350 from madsmtm/nsstring-cfg-gate
cfg-gate `ns_string!` static functionality
2 parents 0a0c861 + 82ce978 commit 2bd99a8

File tree

16 files changed

+587
-643
lines changed

16 files changed

+587
-643
lines changed

.github/workflows/ci.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -481,12 +481,13 @@ jobs:
481481
cargo test $ARGS $PUBLIC_CRATES -ptests
482482
--features=$INTERESTING_FEATURES,catch-all,Foundation_all,$UNSTABLE_FEATURES
483483
484-
# TODO: Re-enable this on Foundation once we do some form of
484+
# TODO: Re-enable this on all of Foundation once we do some form of
485485
# availability checking.
486486
- name: Test static class and selectors
487487
run: >-
488488
cargo test $ARGS $PUBLIC_CRATES -ptests
489-
--features=unstable-static-sel,unstable-static-class
489+
--features=unstable-static-sel,unstable-static-class,unstable-static-nsstring
490+
--features=Foundation,Foundation_NSString
490491
491492
test-ios:
492493
# if: ${{ env.FULL }}

crates/icrate/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ unstable-docsrs = []
110110
# exist.
111111
unstable-private = []
112112

113+
# Make the `ns_string!` macro create the string statically.
114+
#
115+
# Please test it, and report any issues you may find:
116+
# https://github.com/madsmtm/objc2/issues/new
117+
unstable-static-nsstring = []
118+
113119
# Frameworks
114120

115121
AppKit = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use core::mem::ManuallyDrop;
2+
use core::ptr;
3+
use core::sync::atomic::{AtomicPtr, Ordering};
4+
5+
use objc2::rc::{Id, Shared};
6+
use objc2::Message;
7+
8+
/// Allows storing an `Id` in a static and lazily loading it.
9+
pub struct CachedId<T> {
10+
ptr: AtomicPtr<T>,
11+
}
12+
13+
impl<T> CachedId<T> {
14+
/// Constructs a new [`CachedId`].
15+
pub const fn new() -> Self {
16+
Self {
17+
ptr: AtomicPtr::new(ptr::null_mut()),
18+
}
19+
}
20+
}
21+
22+
impl<T: Message> CachedId<T> {
23+
/// Returns the cached object. If no object is yet cached, creates one
24+
/// from the given closure and stores it.
25+
#[inline]
26+
pub fn get(&self, f: impl FnOnce() -> Id<T, Shared>) -> &'static T {
27+
// TODO: Investigate if we can use weaker orderings.
28+
let ptr = self.ptr.load(Ordering::SeqCst);
29+
// SAFETY: The pointer is either NULL, or has been created below.
30+
unsafe { ptr.as_ref() }.unwrap_or_else(|| {
31+
// "Forget" about releasing the object, effectively promoting it
32+
// to a static.
33+
let s = ManuallyDrop::new(f());
34+
let ptr = Id::as_ptr(&s);
35+
self.ptr.store(ptr as *mut T, Ordering::SeqCst);
36+
// SAFETY: The pointer is valid, and will always be valid, since
37+
// we haven't released it.
38+
unsafe { ptr.as_ref().unwrap_unchecked() }
39+
})
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod cached;
2+
mod ns_string;
3+
4+
pub use self::cached::CachedId;
5+
pub use self::ns_string::*;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
#![cfg(feature = "Foundation_NSString")]
2+
//! Macro for making a static NSString.
3+
//!
4+
//! This basically does what clang does, see:
5+
//! - Apple: <https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CodeGenModule.cpp#L5057-L5249>
6+
//! - GNUStep 2.0 (not yet supported): <https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CGObjCGNU.cpp#L973-L1118>
7+
//! - Other (not yet supported): <https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CGObjCGNU.cpp#L2471-L2507>
8+
//!
9+
//! Note that this uses the `CFString` static, while `clang` has support for
10+
//! generating a pure `NSString`. We don't support that yet (since I don't
11+
//! know the use-case), but we definitely could!
12+
//! See: <https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CGObjCMac.cpp#L2007-L2068>
13+
//!
14+
//! See also the following crates that implement UTF-16 conversion:
15+
//! `utf16_lit`, `windows`, `const_utf16`, `wide-literals`, ...
16+
use core::ffi::c_void;
17+
18+
use crate::Foundation::NSString;
19+
use objc2::runtime::Class;
20+
21+
// This is defined in CoreFoundation, but we don't emit a link attribute
22+
// here because it is already linked via Foundation.
23+
//
24+
// Although this is a "private" (underscored) symbol, it is directly
25+
// referenced in Objective-C binaries. So it's safe for us to reference.
26+
extern "C" {
27+
pub static __CFConstantStringClassReference: Class;
28+
}
29+
30+
/// Structure used to describe a constant `CFString`.
31+
///
32+
/// This struct is the same as [`CF_CONST_STRING`], which contains
33+
/// [`CFRuntimeBase`]. While the documentation clearly says that the ABI of
34+
/// `CFRuntimeBase` should not be relied on, we can rely on it as long as we
35+
/// only do it with regards to `CFString` (because `clang` does this as well).
36+
///
37+
/// [`CFRuntimeBase`]: <https://github.com/apple-oss-distributions/CF/blob/CF-1153.18/CFRuntime.h#L216-L228>
38+
/// [`CF_CONST_STRING`]: <https://github.com/apple-oss-distributions/CF/blob/CF-1153.18/CFInternal.h#L332-L336>
39+
#[repr(C)]
40+
pub struct CFConstString {
41+
isa: &'static Class,
42+
// Important that we don't just use `usize` here, since that would be
43+
// wrong on big-endian systems!
44+
cfinfo: u32,
45+
#[cfg(target_pointer_width = "64")]
46+
_rc: u32,
47+
data: *const c_void,
48+
len: usize,
49+
}
50+
51+
// Required to place in a `static`.
52+
unsafe impl Sync for CFConstString {}
53+
54+
impl CFConstString {
55+
// From `CFString.c`:
56+
// <https://github.com/apple-oss-distributions/CF/blob/CF-1153.18/CFString.c#L184-L212>
57+
// > !!! Note: Constant CFStrings use the bit patterns:
58+
// > C8 (11001000 = default allocator, not inline, not freed contents; 8-bit; has NULL byte; doesn't have length; is immutable)
59+
// > D0 (11010000 = default allocator, not inline, not freed contents; Unicode; is immutable)
60+
// > The bit usages should not be modified in a way that would effect these bit patterns.
61+
//
62+
// Hence CoreFoundation guarantees that these two are always valid.
63+
//
64+
// The `CFTypeID` of `CFStringRef` is guaranteed to always be 7:
65+
// <https://github.com/apple-oss-distributions/CF/blob/CF-1153.18/CFRuntime.c#L982>
66+
const FLAGS_ASCII: u32 = 0x07_C8;
67+
const FLAGS_UTF16: u32 = 0x07_D0;
68+
69+
pub const unsafe fn new_ascii(isa: &'static Class, data: &'static [u8]) -> Self {
70+
Self {
71+
isa,
72+
cfinfo: Self::FLAGS_ASCII,
73+
#[cfg(target_pointer_width = "64")]
74+
_rc: 0,
75+
data: data.as_ptr().cast(),
76+
// The length does not include the trailing NUL.
77+
len: data.len() - 1,
78+
}
79+
}
80+
81+
pub const unsafe fn new_utf16(isa: &'static Class, data: &'static [u16]) -> Self {
82+
Self {
83+
isa,
84+
cfinfo: Self::FLAGS_UTF16,
85+
#[cfg(target_pointer_width = "64")]
86+
_rc: 0,
87+
data: data.as_ptr().cast(),
88+
// The length does not include the trailing NUL.
89+
len: data.len() - 1,
90+
}
91+
}
92+
93+
#[inline]
94+
pub const fn as_nsstring_const(&self) -> &NSString {
95+
let ptr: *const Self = self;
96+
unsafe { &*ptr.cast::<NSString>() }
97+
}
98+
99+
// This is deliberately not `const` to prevent the result from being used
100+
// in other statics, since that is only possible if the
101+
// `unstable-static-nsstring` feature is enabled.
102+
#[inline]
103+
pub fn as_nsstring(&self) -> &NSString {
104+
self.as_nsstring_const()
105+
}
106+
}
107+
108+
/// Returns `true` if `bytes` is entirely ASCII with no interior NULs.
109+
pub const fn is_ascii_no_nul(bytes: &[u8]) -> bool {
110+
let mut i = 0;
111+
while i < bytes.len() {
112+
let byte = bytes[i];
113+
if !byte.is_ascii() || byte == b'\0' {
114+
return false;
115+
}
116+
i += 1;
117+
}
118+
true
119+
}
120+
121+
pub struct Utf16Char {
122+
pub repr: [u16; 2],
123+
pub len: usize,
124+
}
125+
126+
impl Utf16Char {
127+
const fn encode(ch: u32) -> Self {
128+
if ch <= 0xffff {
129+
Self {
130+
repr: [ch as u16, 0],
131+
len: 1,
132+
}
133+
} else {
134+
let payload = ch - 0x10000;
135+
let hi = (payload >> 10) | 0xd800;
136+
let lo = (payload & 0x3ff) | 0xdc00;
137+
Self {
138+
repr: [hi as u16, lo as u16],
139+
len: 2,
140+
}
141+
}
142+
}
143+
144+
#[cfg(test)]
145+
fn as_slice(&self) -> &[u16] {
146+
&self.repr[..self.len]
147+
}
148+
}
149+
150+
pub struct EncodeUtf16Iter {
151+
str: &'static [u8],
152+
index: usize,
153+
}
154+
155+
impl EncodeUtf16Iter {
156+
pub const fn new(str: &'static [u8]) -> Self {
157+
Self { str, index: 0 }
158+
}
159+
160+
pub const fn next(self) -> Option<(Self, Utf16Char)> {
161+
if self.index >= self.str.len() {
162+
None
163+
} else {
164+
let (index, ch) = decode_utf8(self.str, self.index);
165+
Some((Self { index, ..self }, Utf16Char::encode(ch)))
166+
}
167+
}
168+
}
169+
170+
// (&str bytes, index) -> (new index, decoded char)
171+
const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
172+
let b0 = s[i];
173+
match b0 {
174+
// one-byte seq
175+
0b0000_0000..=0b0111_1111 => {
176+
let decoded = b0 as u32;
177+
(i + 1, decoded)
178+
}
179+
// two-byte seq
180+
0b1100_0000..=0b1101_1111 => {
181+
let decoded = ((b0 as u32 & 0x1f) << 6) | (s[i + 1] as u32 & 0x3f);
182+
(i + 2, decoded)
183+
}
184+
// 3 byte seq
185+
0b1110_0000..=0b1110_1111 => {
186+
let decoded = ((b0 as u32 & 0x0f) << 12)
187+
| ((s[i + 1] as u32 & 0x3f) << 6)
188+
| (s[i + 2] as u32 & 0x3f);
189+
(i + 3, decoded)
190+
}
191+
// 4 byte seq
192+
0b1111_0000..=0b1111_0111 => {
193+
let decoded = ((b0 as u32 & 0x07) << 18)
194+
| ((s[i + 1] as u32 & 0x3f) << 12)
195+
| ((s[i + 2] as u32 & 0x3f) << 6)
196+
| (s[i + 3] as u32 & 0x3f);
197+
(i + 4, decoded)
198+
}
199+
// continuation bytes, or never-valid bytes.
200+
0b1000_0000..=0b1011_1111 | 0b1111_1000..=0b1111_1111 => {
201+
panic!("Encountered invalid bytes")
202+
}
203+
}
204+
}
205+
206+
#[cfg(test)]
207+
mod tests {
208+
use super::*;
209+
210+
#[test]
211+
fn test_is_ascii() {
212+
assert!(is_ascii_no_nul(b"a"));
213+
assert!(is_ascii_no_nul(b"abc"));
214+
215+
assert!(!is_ascii_no_nul(b"\xff"));
216+
217+
assert!(!is_ascii_no_nul(b"\0"));
218+
assert!(!is_ascii_no_nul(b"a\0b"));
219+
assert!(!is_ascii_no_nul(b"ab\0"));
220+
assert!(!is_ascii_no_nul(b"a\0b\0"));
221+
}
222+
223+
#[test]
224+
#[ignore = "slow, enable this if working on ns_string!"]
225+
fn test_decode_utf8() {
226+
for c in '\u{0}'..=core::char::MAX {
227+
let mut buf;
228+
for off in 0..4 {
229+
// Ensure we see garbage if we read outside bounds.
230+
buf = [0xff; 8];
231+
let len = c.encode_utf8(&mut buf[off..(off + 4)]).len();
232+
let (end_idx, decoded) = decode_utf8(&buf, off);
233+
assert_eq!(
234+
(end_idx, decoded),
235+
(off + len, c as u32),
236+
"failed for U+{code:04X} ({ch:?}) encoded as {buf:#x?} over {range:?}",
237+
code = c as u32,
238+
ch = c,
239+
buf = &buf[off..(off + len)],
240+
range = off..(off + len),
241+
);
242+
}
243+
}
244+
}
245+
246+
#[test]
247+
#[ignore = "slow, enable this if working on ns_string!"]
248+
fn encode_utf16() {
249+
for c in '\u{0}'..=core::char::MAX {
250+
assert_eq!(
251+
c.encode_utf16(&mut [0u16; 2]),
252+
Utf16Char::encode(c as u32).as_slice(),
253+
"failed for U+{:04X} ({:?})",
254+
c as u32,
255+
c
256+
);
257+
}
258+
}
259+
}

0 commit comments

Comments
 (0)