web engine - experimental web browser

Implement CoreFoundation minimal bindings

Add platform/src/cf.rs with:
- Raw extern "C" bindings to CoreFoundation.framework: CFStringCreateWithBytes,
CFRetain, CFRelease, CFStringGetLength, CFStringGetCString
- CFStringRef type alias and CFStringEncoding constants
- CfString RAII wrapper: creates CFStringRef from &str, calls CFRelease on drop
- Clone impl that calls CFRetain for proper reference counting
- Debug/Display impls, len/is_empty helpers, to_string_lossy for round-tripping
- cf_retain/cf_release public wrappers for manual reference counting
- 10 unit tests covering creation, length, empty strings, unicode, emoji,
clone/drop, debug/display formatting, and pointer validity

This is needed for passing string parameters to AppKit APIs (window titles,
menu items, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+286
+285
crates/platform/src/cf.rs
··· 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 + 12 + use std::fmt; 13 + use std::os::raw::c_void; 14 + use std::ptr::NonNull; 15 + 16 + // --------------------------------------------------------------------------- 17 + // Raw FFI types 18 + // --------------------------------------------------------------------------- 19 + 20 + /// Opaque CoreFoundation string type. 21 + #[repr(C)] 22 + pub struct __CFString { 23 + _private: [u8; 0], 24 + } 25 + 26 + /// A reference to a CoreFoundation string (immutable). 27 + pub type CFStringRef = *const __CFString; 28 + 29 + /// CoreFoundation index/size type. 30 + pub type CFIndex = isize; 31 + 32 + /// CoreFoundation string encoding identifier. 33 + pub type CFStringEncoding = u32; 34 + 35 + /// CoreFoundation Boolean type. 36 + pub type CFBoolean = u8; 37 + 38 + /// UTF-8 encoding constant. 39 + pub 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")] 46 + extern "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] 77 + pub 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] 88 + pub 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_ref`] for passing 100 + /// to AppKit APIs. 101 + pub struct CfString(NonNull<__CFString>); 102 + 103 + impl 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 + 175 + impl Clone for CfString { 176 + fn clone(&self) -> CfString { 177 + unsafe { cf_retain(self.as_void_ptr()) }; 178 + CfString(self.0) 179 + } 180 + } 181 + 182 + impl Drop for CfString { 183 + fn drop(&mut self) { 184 + unsafe { cf_release(self.as_void_ptr()) }; 185 + } 186 + } 187 + 188 + impl 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 + 194 + impl 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. 202 + unsafe impl Send for CfString {} 203 + unsafe impl Sync for CfString {} 204 + 205 + // --------------------------------------------------------------------------- 206 + // Tests 207 + // --------------------------------------------------------------------------- 208 + 209 + #[cfg(test)] 210 + mod 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 + }
+1
crates/platform/src/lib.rs
··· 1 1 //! Minimal macOS platform layer — Obj-C FFI, AppKit, CoreGraphics, Metal. 2 2 3 + pub mod cf; 3 4 pub mod objc;