web engine - experimental web browser
1//! Minimal CoreFoundation bindings.
2//!
3//! Provides `CFStringRef` creation from Rust strings and RAII reference
4//! counting via [`CfString`]. These bindings are needed for passing string
5//! parameters to AppKit APIs (window titles, menu items, etc).
6//!
7//! # Safety
8//!
9//! This module contains `unsafe` code for FFI with CoreFoundation.
10//! The `platform` crate is one of the few crates where `unsafe` is permitted.
11
12use std::fmt;
13use std::os::raw::c_void;
14use std::ptr::NonNull;
15
16// ---------------------------------------------------------------------------
17// Raw FFI types
18// ---------------------------------------------------------------------------
19
20/// Opaque CoreFoundation string type.
21#[repr(C)]
22pub struct __CFString {
23 _private: [u8; 0],
24}
25
26/// A reference to a CoreFoundation string (immutable).
27pub type CFStringRef = *const __CFString;
28
29/// CoreFoundation index/size type.
30pub type CFIndex = isize;
31
32/// CoreFoundation string encoding identifier.
33pub type CFStringEncoding = u32;
34
35/// CoreFoundation Boolean type.
36pub type CFBoolean = u8;
37
38/// UTF-8 encoding constant.
39pub const K_CF_STRING_ENCODING_UTF8: CFStringEncoding = 0x0800_0100;
40
41// ---------------------------------------------------------------------------
42// Raw FFI bindings to CoreFoundation.framework
43// ---------------------------------------------------------------------------
44
45#[link(name = "CoreFoundation", kind = "framework")]
46extern "C" {
47 fn CFStringCreateWithBytes(
48 alloc: *const c_void,
49 bytes: *const u8,
50 num_bytes: CFIndex,
51 encoding: CFStringEncoding,
52 is_external_representation: CFBoolean,
53 ) -> CFStringRef;
54
55 fn CFRetain(cf: *const c_void) -> *const c_void;
56 fn CFRelease(cf: *const c_void);
57
58 fn CFStringGetLength(string: CFStringRef) -> CFIndex;
59 fn CFStringGetCString(
60 string: CFStringRef,
61 buffer: *mut u8,
62 buffer_size: CFIndex,
63 encoding: CFStringEncoding,
64 ) -> CFBoolean;
65}
66
67// ---------------------------------------------------------------------------
68// Public wrappers for CFRetain / CFRelease
69// ---------------------------------------------------------------------------
70
71/// Increment the reference count of a CoreFoundation object.
72///
73/// # Safety
74///
75/// `ptr` must point to a valid CoreFoundation object.
76#[inline]
77pub unsafe fn cf_retain(ptr: *const c_void) -> *const c_void {
78 CFRetain(ptr)
79}
80
81/// Decrement the reference count of a CoreFoundation object.
82///
83/// # Safety
84///
85/// `ptr` must point to a valid CoreFoundation object with a positive
86/// reference count. The object may be deallocated after this call.
87#[inline]
88pub unsafe fn cf_release(ptr: *const c_void) {
89 CFRelease(ptr);
90}
91
92// ---------------------------------------------------------------------------
93// CfString — RAII wrapper around CFStringRef
94// ---------------------------------------------------------------------------
95
96/// An owned CoreFoundation string that calls `CFRelease` on drop.
97///
98/// Created from a Rust `&str` via [`CfString::new`]. The underlying
99/// `CFStringRef` can be borrowed with [`CfString::as_cf_ref`] for passing
100/// to AppKit APIs.
101pub struct CfString(NonNull<__CFString>);
102
103impl CfString {
104 /// Create a `CfString` from a Rust string slice.
105 ///
106 /// Returns `None` if CoreFoundation fails to create the string (should
107 /// only happen for extremely large strings or out-of-memory conditions).
108 pub fn new(s: &str) -> Option<CfString> {
109 let ptr = unsafe {
110 CFStringCreateWithBytes(
111 std::ptr::null(), // default allocator
112 s.as_ptr(),
113 s.len() as CFIndex,
114 K_CF_STRING_ENCODING_UTF8,
115 0, // not external representation
116 )
117 };
118 // CFStringCreateWithBytes returns null on failure.
119 NonNull::new(ptr as *mut __CFString).map(CfString)
120 }
121
122 /// Return the raw `CFStringRef` for passing to CoreFoundation / AppKit APIs.
123 #[inline]
124 pub fn as_cf_ref(&self) -> CFStringRef {
125 self.0.as_ptr() as CFStringRef
126 }
127
128 /// Return the raw pointer as `*const c_void` for APIs that take a generic
129 /// CoreFoundation type reference.
130 #[inline]
131 pub fn as_void_ptr(&self) -> *const c_void {
132 self.0.as_ptr() as *const c_void
133 }
134
135 /// Get the length of the string in UTF-16 code units.
136 pub fn len(&self) -> usize {
137 let len = unsafe { CFStringGetLength(self.as_cf_ref()) };
138 len as usize
139 }
140
141 /// Returns `true` if the string is empty.
142 pub fn is_empty(&self) -> bool {
143 self.len() == 0
144 }
145
146 /// Try to extract the string contents as a Rust `String`.
147 ///
148 /// This is primarily useful for testing and debugging.
149 pub fn to_string_lossy(&self) -> String {
150 // Allocate a buffer large enough for the UTF-8 representation.
151 // CFStringGetCString needs space for a NUL terminator.
152 // Worst case: each UTF-16 code unit -> 3 UTF-8 bytes + NUL.
153 let max_len = self.len() * 3 + 1;
154 let mut buf = vec![0u8; max_len];
155
156 let ok = unsafe {
157 CFStringGetCString(
158 self.as_cf_ref(),
159 buf.as_mut_ptr(),
160 buf.len() as CFIndex,
161 K_CF_STRING_ENCODING_UTF8,
162 )
163 };
164
165 if ok != 0 {
166 // Find the NUL terminator.
167 let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
168 String::from_utf8_lossy(&buf[..nul_pos]).into_owned()
169 } else {
170 String::new()
171 }
172 }
173}
174
175impl Clone for CfString {
176 fn clone(&self) -> CfString {
177 unsafe { cf_retain(self.as_void_ptr()) };
178 CfString(self.0)
179 }
180}
181
182impl Drop for CfString {
183 fn drop(&mut self) {
184 unsafe { cf_release(self.as_void_ptr()) };
185 }
186}
187
188impl fmt::Debug for CfString {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "CfString(\"{}\")", self.to_string_lossy())
191 }
192}
193
194impl fmt::Display for CfString {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(f, "{}", self.to_string_lossy())
197 }
198}
199
200// SAFETY: CFString is an immutable, reference-counted CoreFoundation type.
201// CFRetain/CFRelease are thread-safe.
202unsafe impl Send for CfString {}
203unsafe impl Sync for CfString {}
204
205// ---------------------------------------------------------------------------
206// Tests
207// ---------------------------------------------------------------------------
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn create_cfstring_from_str() {
215 let s = CfString::new("Hello, world!").expect("should create CFString");
216 assert_eq!(s.to_string_lossy(), "Hello, world!");
217 }
218
219 #[test]
220 fn cfstring_length() {
221 let s = CfString::new("abc").expect("should create CFString");
222 assert_eq!(s.len(), 3);
223 }
224
225 #[test]
226 fn cfstring_empty() {
227 let s = CfString::new("").expect("should create empty CFString");
228 assert!(s.is_empty());
229 assert_eq!(s.len(), 0);
230 assert_eq!(s.to_string_lossy(), "");
231 }
232
233 #[test]
234 fn cfstring_unicode() {
235 let s = CfString::new("caf\u{00e9}").expect("should handle unicode");
236 assert_eq!(s.to_string_lossy(), "caf\u{00e9}");
237 // "caf\u{e9}" is 4 UTF-16 code units
238 assert_eq!(s.len(), 4);
239 }
240
241 #[test]
242 fn cfstring_emoji() {
243 // Emoji outside BMP: encoded as surrogate pair in UTF-16 (2 code units)
244 let s = CfString::new("\u{1F600}").expect("should handle emoji");
245 assert_eq!(s.to_string_lossy(), "\u{1F600}");
246 assert_eq!(s.len(), 2); // surrogate pair
247 }
248
249 #[test]
250 fn cfstring_clone_and_drop() {
251 let s = CfString::new("test clone").expect("should create");
252 let s2 = s.clone();
253 assert_eq!(s.to_string_lossy(), s2.to_string_lossy());
254 // Both should be droppable without crash (refcount was incremented).
255 drop(s);
256 assert_eq!(s2.to_string_lossy(), "test clone");
257 }
258
259 #[test]
260 fn cfstring_debug_format() {
261 let s = CfString::new("debug").expect("should create");
262 let debug = format!("{:?}", s);
263 assert!(debug.contains("CfString"));
264 assert!(debug.contains("debug"));
265 }
266
267 #[test]
268 fn cfstring_display_format() {
269 let s = CfString::new("display").expect("should create");
270 let display = format!("{}", s);
271 assert_eq!(display, "display");
272 }
273
274 #[test]
275 fn cfstring_as_cf_ref_not_null() {
276 let s = CfString::new("ptr test").expect("should create");
277 assert!(!s.as_cf_ref().is_null());
278 }
279
280 #[test]
281 fn cfstring_as_void_ptr_not_null() {
282 let s = CfString::new("void test").expect("should create");
283 assert!(!s.as_void_ptr().is_null());
284 }
285}