Skip to content

Commit 42bd737

Browse files
committed
Add GNUStep shim for ns_string! macro
This means we have to remove support for putting string references in statics for now at least
1 parent 504543d commit 42bd737

File tree

9 files changed

+400
-54
lines changed

9 files changed

+400
-54
lines changed

objc2-foundation/src/__string_macro.rs

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
//! generating a pure `NSString`. We don't support that yet (since I don't
1010
//! know the use-case), but we definitely could!
1111
//! See: <https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CGObjCMac.cpp#L2007-L2068>
12-
#![cfg(feature = "apple")]
1312
use core::ffi::c_void;
13+
use core::mem::ManuallyDrop;
14+
use core::ptr;
15+
use core::sync::atomic::{AtomicPtr, Ordering};
1416

17+
use objc2::rc::Id;
1518
use objc2::runtime::Class;
1619

1720
use crate::NSString;
@@ -89,10 +92,17 @@ impl CFConstString {
8992
}
9093

9194
#[inline]
92-
pub const fn as_nsstring(&self) -> &NSString {
95+
pub const fn as_nsstring_const(&self) -> &NSString {
9396
let ptr: *const Self = self;
9497
unsafe { &*ptr.cast::<NSString>() }
9598
}
99+
100+
// This is deliberately not `const` to prevent the result from being used
101+
// in other statics, since not all platforms support that (yet).
102+
#[inline]
103+
pub fn as_nsstring(&self) -> &NSString {
104+
self.as_nsstring_const()
105+
}
96106
}
97107

98108
/// Returns `true` if `bytes` is entirely ASCII with no interior NULs.
@@ -193,6 +203,40 @@ const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
193203
}
194204
}
195205

206+
/// Allows storing a [`NSString`] in a static and lazily loading it.
207+
#[doc(hidden)]
208+
pub struct CachedNSString {
209+
ptr: AtomicPtr<NSString>,
210+
}
211+
212+
impl CachedNSString {
213+
/// Constructs a new [`CachedNSString`].
214+
pub const fn new() -> Self {
215+
Self {
216+
ptr: AtomicPtr::new(ptr::null_mut()),
217+
}
218+
}
219+
220+
/// Returns the cached NSString. If no string is yet cached, creates one
221+
/// with the given name and stores it.
222+
#[inline]
223+
pub fn get(&self, s: &str) -> &'static NSString {
224+
// TODO: Investigate if we can use weaker orderings.
225+
let ptr = self.ptr.load(Ordering::SeqCst);
226+
// SAFETY: The pointer is either NULL, or has been created below.
227+
unsafe { ptr.as_ref() }.unwrap_or_else(|| {
228+
// "Forget" about releasing the string, effectively promoting it
229+
// to a static.
230+
let s = ManuallyDrop::new(NSString::from_str(s));
231+
let ptr = Id::as_ptr(&s);
232+
self.ptr.store(ptr as *mut NSString, Ordering::SeqCst);
233+
// SAFETY: The pointer is valid, and will always be valid, since
234+
// we haven't released it.
235+
unsafe { ptr.as_ref().unwrap_unchecked() }
236+
})
237+
}
238+
}
239+
196240
/// Creates an [`NSString`][`crate::NSString`] from a static string.
197241
///
198242
/// Currently only supported on Apple targets.
@@ -201,30 +245,20 @@ const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
201245
/// # Examples
202246
///
203247
/// This macro takes a either a `"string"` literal or `const` string slice as
204-
/// the argument:
248+
/// the argument, and produces a `&'static NSString`:
205249
///
206250
/// ```
207-
/// use objc2_foundation::ns_string;
208-
/// let hello = ns_string!("hello");
251+
/// use objc2_foundation::{ns_string, NSString};
252+
/// # #[cfg(feature = "gnustep-1-7")]
253+
/// # unsafe { objc2::__gnustep_hack::get_class_to_force_linkage() };
254+
/// let hello: &'static NSString = ns_string!("hello");
209255
/// assert_eq!(hello.to_string(), "hello");
210256
///
211257
/// const WORLD: &str = "world";
212258
/// let world = ns_string!(WORLD);
213259
/// assert_eq!(world.to_string(), WORLD);
214260
/// ```
215261
///
216-
/// The result of this macro can even be used to create `static` values:
217-
///
218-
/// ```
219-
/// # use objc2_foundation::{ns_string, NSString};
220-
/// static WORLD: &NSString = ns_string!("world");
221-
///
222-
/// assert_eq!(WORLD.to_string(), "world");
223-
/// ```
224-
///
225-
/// Note that the result cannot be used in a `const` because it refers to
226-
/// static data outside of this library.
227-
///
228262
///
229263
/// # Unicode Strings
230264
///
@@ -233,9 +267,11 @@ const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
233267
/// string to the most efficient encoding, you don't have to do anything!
234268
///
235269
/// ```
236-
/// # use objc2_foundation::{ns_string, NSString};
237-
/// static HELLO_RU: &NSString = ns_string!("Привет");
238-
/// assert_eq!(HELLO_RU.to_string(), "Привет");
270+
/// # use objc2_foundation::ns_string;
271+
/// # #[cfg(feature = "gnustep-1-7")]
272+
/// # unsafe { objc2::__gnustep_hack::get_class_to_force_linkage() };
273+
/// let hello_ru = ns_string!("Привет");
274+
/// assert_eq!(hello_ru.to_string(), "Привет");
239275
/// ```
240276
///
241277
/// Note that because this is implemented with `const` evaluation, massive
@@ -250,6 +286,8 @@ const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
250286
///
251287
/// ```
252288
/// # use objc2_foundation::ns_string;
289+
/// # #[cfg(feature = "gnustep-1-7")]
290+
/// # unsafe { objc2::__gnustep_hack::get_class_to_force_linkage() };
253291
/// let example = ns_string!("example\0");
254292
/// assert_eq!(example.to_string(), "example\0");
255293
///
@@ -269,40 +307,54 @@ const fn decode_utf8(s: &[u8], i: usize) -> (usize, u32) {
269307
///
270308
/// [`NSString::from_str`]: crate::NSString::from_str
271309
#[macro_export]
272-
#[cfg(feature = "apple")] // To make `auto_doc_cfg` pick this up
273310
macro_rules! ns_string {
274311
($s:expr) => {{
312+
// Immediately place in constant for better UI
313+
const INPUT: &str = $s;
314+
$crate::__ns_string_inner!(INPUT)
315+
}};
316+
}
317+
318+
#[doc(hidden)]
319+
#[cfg(feature = "apple")]
320+
#[macro_export]
321+
macro_rules! __ns_string_inner {
322+
($inp:ident) => {{
323+
const X: &[u8] = $inp.as_bytes();
324+
$crate::__ns_string_inner!(@inner X);
325+
// Return &'static NSString
326+
CFSTRING.as_nsstring()
327+
}};
328+
(@inner $inp:ident) => {
275329
// Note: We create both the ASCII + NUL and the UTF-16 + NUL versions
276330
// of the string, since we can't conditionally create a static.
277331
//
278332
// Since we don't add the `#[used]` attribute, Rust can fairly easily
279333
// figure out that one of the variants are never used, and simply
280334
// exclude it.
281335

282-
const INPUT: &[u8] = $s.as_bytes();
283-
284336
// Convert the input slice to a C-style string with a NUL byte.
285337
//
286338
// The section is the same as what clang sets, see:
287339
// https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CodeGenModule.cpp#L5192
288340
#[link_section = "__TEXT,__cstring,cstring_literals"]
289-
static ASCII: [u8; INPUT.len() + 1] = {
290-
// Zero-fill with INPUT.len() + 1
291-
let mut res: [u8; INPUT.len() + 1] = [0; INPUT.len() + 1];
341+
static ASCII: [u8; $inp.len() + 1] = {
342+
// Zero-fill with $inp.len() + 1
343+
let mut res: [u8; $inp.len() + 1] = [0; $inp.len() + 1];
292344
let mut i = 0;
293-
// Fill with data from INPUT
294-
while i < INPUT.len() {
295-
res[i] = INPUT[i];
345+
// Fill with data from $inp
346+
while i < $inp.len() {
347+
res[i] = $inp[i];
296348
i += 1;
297349
}
298-
// Now contains INPUT + '\0'
350+
// Now contains $inp + '\0'
299351
res
300352
};
301353

302354
// The full UTF-16 contents along with the written length.
303-
const UTF16_FULL: (&[u16; INPUT.len()], usize) = {
304-
let mut out = [0u16; INPUT.len()];
305-
let mut iter = $crate::__string_macro::EncodeUtf16Iter::new(INPUT);
355+
const UTF16_FULL: (&[u16; $inp.len()], usize) = {
356+
let mut out = [0u16; $inp.len()];
357+
let mut iter = $crate::__string_macro::EncodeUtf16Iter::new($inp);
306358
let mut written = 0;
307359

308360
while let Some((state, chars)) = iter.next() {
@@ -344,7 +396,7 @@ macro_rules! ns_string {
344396
// https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/CodeGen/CodeGenModule.cpp#L5243
345397
#[link_section = "__DATA,__cfstring"]
346398
static CFSTRING: $crate::__string_macro::CFConstString = unsafe {
347-
if $crate::__string_macro::is_ascii_no_nul(INPUT) {
399+
if $crate::__string_macro::is_ascii_no_nul($inp) {
348400
// This is technically an optimization (UTF-16 strings are
349401
// always valid), but it's a fairly important one!
350402
$crate::__string_macro::CFConstString::new_ascii(
@@ -358,9 +410,17 @@ macro_rules! ns_string {
358410
)
359411
}
360412
};
413+
};
414+
}
361415

362-
// Return &'static NSString
363-
CFSTRING.as_nsstring()
416+
#[doc(hidden)]
417+
#[cfg(not(feature = "apple"))]
418+
#[macro_export]
419+
macro_rules! __ns_string_inner {
420+
($inp:ident) => {{
421+
use $crate::__string_macro::CachedNSString;
422+
static CACHED_NSSTRING: CachedNSString = CachedNSString::new();
423+
CACHED_NSSTRING.get($inp)
364424
}};
365425
}
366426

@@ -422,14 +482,14 @@ mod tests {
422482
fn ns_string() {
423483
macro_rules! test {
424484
($($s:expr,)+) => {$({
425-
static STRING: &NSString = ns_string!($s);
426-
let s = NSString::from_str($s);
485+
let s1 = ns_string!($s);
486+
let s2 = NSString::from_str($s);
427487

428-
assert_eq!(STRING, STRING);
429-
assert_eq!(STRING, &*s);
488+
assert_eq!(s1, s1);
489+
assert_eq!(s1, &*s2);
430490

431-
assert_eq!(STRING.to_string(), $s);
432-
assert_eq!(s.to_string(), $s);
491+
assert_eq!(s1.to_string(), $s);
492+
assert_eq!(s2.to_string(), $s);
433493
})+};
434494
}
435495

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,141 @@
11
.text
22
.intel_syntax noprefix
3+
.section .text.get_ascii,"ax",@progbits
4+
.globl get_ascii
5+
.p2align 4, 0x90
6+
.type get_ascii,@function
7+
get_ascii:
8+
push ebx
9+
sub esp, 8
10+
call .L0$pb
11+
.L0$pb:
12+
pop ebx
13+
.Ltmp0:
14+
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb)
15+
mov eax, dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0@GOTOFF]
16+
test eax, eax
17+
je .LBB0_1
18+
add esp, 8
19+
pop ebx
20+
ret
21+
.LBB0_1:
22+
sub esp, 8
23+
lea eax, [ebx + .Lanon.ebccacd9e14cd5382d61cd57489efe98.0@GOTOFF]
24+
push 3
25+
push eax
26+
call SYM(objc2_foundation::string::NSString::from_str::GENERATED_ID, 0)@PLT
27+
add esp, 16
28+
mov ecx, eax
29+
xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0@GOTOFF], ecx
30+
add esp, 8
31+
pop ebx
32+
ret
33+
.Lfunc_end0:
34+
.size get_ascii, .Lfunc_end0-get_ascii
35+
36+
.section .text.get_utf16,"ax",@progbits
37+
.globl get_utf16
38+
.p2align 4, 0x90
39+
.type get_utf16,@function
40+
get_utf16:
41+
push ebx
42+
sub esp, 8
43+
call .L1$pb
44+
.L1$pb:
45+
pop ebx
46+
.Ltmp1:
47+
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp1-.L1$pb)
48+
mov eax, dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0@GOTOFF]
49+
test eax, eax
50+
je .LBB1_1
51+
add esp, 8
52+
pop ebx
53+
ret
54+
.LBB1_1:
55+
sub esp, 8
56+
lea eax, [ebx + .Lanon.ebccacd9e14cd5382d61cd57489efe98.1@GOTOFF]
57+
push 5
58+
push eax
59+
call SYM(objc2_foundation::string::NSString::from_str::GENERATED_ID, 0)@PLT
60+
add esp, 16
61+
mov ecx, eax
62+
xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0@GOTOFF], ecx
63+
add esp, 8
64+
pop ebx
65+
ret
66+
.Lfunc_end1:
67+
.size get_utf16, .Lfunc_end1-get_utf16
68+
69+
.section .text.get_with_nul,"ax",@progbits
70+
.globl get_with_nul
71+
.p2align 4, 0x90
72+
.type get_with_nul,@function
73+
get_with_nul:
74+
push ebx
75+
sub esp, 8
76+
call .L2$pb
77+
.L2$pb:
78+
pop ebx
79+
.Ltmp2:
80+
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp2-.L2$pb)
81+
mov eax, dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0@GOTOFF]
82+
test eax, eax
83+
je .LBB2_1
84+
add esp, 8
85+
pop ebx
86+
ret
87+
.LBB2_1:
88+
sub esp, 8
89+
lea eax, [ebx + .Lanon.ebccacd9e14cd5382d61cd57489efe98.2@GOTOFF]
90+
push 6
91+
push eax
92+
call SYM(objc2_foundation::string::NSString::from_str::GENERATED_ID, 0)@PLT
93+
add esp, 16
94+
mov ecx, eax
95+
xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0@GOTOFF], ecx
96+
add esp, 8
97+
pop ebx
98+
ret
99+
.Lfunc_end2:
100+
.size get_with_nul, .Lfunc_end2-get_with_nul
101+
102+
.type .Lanon.ebccacd9e14cd5382d61cd57489efe98.0,@object
103+
.section .rodata..Lanon.ebccacd9e14cd5382d61cd57489efe98.0,"a",@progbits
104+
.Lanon.ebccacd9e14cd5382d61cd57489efe98.0:
105+
.ascii "abc"
106+
.size .Lanon.ebccacd9e14cd5382d61cd57489efe98.0, 3
107+
108+
.type .Lanon.ebccacd9e14cd5382d61cd57489efe98.1,@object
109+
.section .rodata..Lanon.ebccacd9e14cd5382d61cd57489efe98.1,"a",@progbits
110+
.Lanon.ebccacd9e14cd5382d61cd57489efe98.1:
111+
.ascii "\303\241b\304\207"
112+
.size .Lanon.ebccacd9e14cd5382d61cd57489efe98.1, 5
113+
114+
.type .Lanon.ebccacd9e14cd5382d61cd57489efe98.2,@object
115+
.section .rodata..Lanon.ebccacd9e14cd5382d61cd57489efe98.2,"a",@progbits
116+
.Lanon.ebccacd9e14cd5382d61cd57489efe98.2:
117+
.asciz "a\000b\000c"
118+
.size .Lanon.ebccacd9e14cd5382d61cd57489efe98.2, 6
119+
120+
.type SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0,@object
121+
.section .bss.SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0,"aw",@nobits
122+
.p2align 2
123+
SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0:
124+
.long 0
125+
.size SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0, 4
126+
127+
.type SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0,@object
128+
.section .bss.SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0,"aw",@nobits
129+
.p2align 2
130+
SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0:
131+
.long 0
132+
.size SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0, 4
133+
134+
.type SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0,@object
135+
.section .bss.SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0,"aw",@nobits
136+
.p2align 2
137+
SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0:
138+
.long 0
139+
.size SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0, 4
140+
3141
.section ".note.GNU-stack","",@progbits

0 commit comments

Comments
 (0)