web engine - experimental web browser
at x25519 285 lines 8.8 kB view raw
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}