//! Minimal CoreFoundation bindings. //! //! Provides `CFStringRef` creation from Rust strings and RAII reference //! counting via [`CfString`]. These bindings are needed for passing string //! parameters to AppKit APIs (window titles, menu items, etc). //! //! # Safety //! //! This module contains `unsafe` code for FFI with CoreFoundation. //! The `platform` crate is one of the few crates where `unsafe` is permitted. use std::fmt; use std::os::raw::c_void; use std::ptr::NonNull; // --------------------------------------------------------------------------- // Raw FFI types // --------------------------------------------------------------------------- /// Opaque CoreFoundation string type. #[repr(C)] pub struct __CFString { _private: [u8; 0], } /// A reference to a CoreFoundation string (immutable). pub type CFStringRef = *const __CFString; /// CoreFoundation index/size type. pub type CFIndex = isize; /// CoreFoundation string encoding identifier. pub type CFStringEncoding = u32; /// CoreFoundation Boolean type. pub type CFBoolean = u8; /// UTF-8 encoding constant. pub const K_CF_STRING_ENCODING_UTF8: CFStringEncoding = 0x0800_0100; // --------------------------------------------------------------------------- // Raw FFI bindings to CoreFoundation.framework // --------------------------------------------------------------------------- #[link(name = "CoreFoundation", kind = "framework")] extern "C" { fn CFStringCreateWithBytes( alloc: *const c_void, bytes: *const u8, num_bytes: CFIndex, encoding: CFStringEncoding, is_external_representation: CFBoolean, ) -> CFStringRef; fn CFRetain(cf: *const c_void) -> *const c_void; fn CFRelease(cf: *const c_void); fn CFStringGetLength(string: CFStringRef) -> CFIndex; fn CFStringGetCString( string: CFStringRef, buffer: *mut u8, buffer_size: CFIndex, encoding: CFStringEncoding, ) -> CFBoolean; } // --------------------------------------------------------------------------- // Public wrappers for CFRetain / CFRelease // --------------------------------------------------------------------------- /// Increment the reference count of a CoreFoundation object. /// /// # Safety /// /// `ptr` must point to a valid CoreFoundation object. #[inline] pub unsafe fn cf_retain(ptr: *const c_void) -> *const c_void { CFRetain(ptr) } /// Decrement the reference count of a CoreFoundation object. /// /// # Safety /// /// `ptr` must point to a valid CoreFoundation object with a positive /// reference count. The object may be deallocated after this call. #[inline] pub unsafe fn cf_release(ptr: *const c_void) { CFRelease(ptr); } // --------------------------------------------------------------------------- // CfString — RAII wrapper around CFStringRef // --------------------------------------------------------------------------- /// An owned CoreFoundation string that calls `CFRelease` on drop. /// /// Created from a Rust `&str` via [`CfString::new`]. The underlying /// `CFStringRef` can be borrowed with [`CfString::as_cf_ref`] for passing /// to AppKit APIs. pub struct CfString(NonNull<__CFString>); impl CfString { /// Create a `CfString` from a Rust string slice. /// /// Returns `None` if CoreFoundation fails to create the string (should /// only happen for extremely large strings or out-of-memory conditions). pub fn new(s: &str) -> Option { let ptr = unsafe { CFStringCreateWithBytes( std::ptr::null(), // default allocator s.as_ptr(), s.len() as CFIndex, K_CF_STRING_ENCODING_UTF8, 0, // not external representation ) }; // CFStringCreateWithBytes returns null on failure. NonNull::new(ptr as *mut __CFString).map(CfString) } /// Return the raw `CFStringRef` for passing to CoreFoundation / AppKit APIs. #[inline] pub fn as_cf_ref(&self) -> CFStringRef { self.0.as_ptr() as CFStringRef } /// Return the raw pointer as `*const c_void` for APIs that take a generic /// CoreFoundation type reference. #[inline] pub fn as_void_ptr(&self) -> *const c_void { self.0.as_ptr() as *const c_void } /// Get the length of the string in UTF-16 code units. pub fn len(&self) -> usize { let len = unsafe { CFStringGetLength(self.as_cf_ref()) }; len as usize } /// Returns `true` if the string is empty. pub fn is_empty(&self) -> bool { self.len() == 0 } /// Try to extract the string contents as a Rust `String`. /// /// This is primarily useful for testing and debugging. pub fn to_string_lossy(&self) -> String { // Allocate a buffer large enough for the UTF-8 representation. // CFStringGetCString needs space for a NUL terminator. // Worst case: each UTF-16 code unit -> 3 UTF-8 bytes + NUL. let max_len = self.len() * 3 + 1; let mut buf = vec![0u8; max_len]; let ok = unsafe { CFStringGetCString( self.as_cf_ref(), buf.as_mut_ptr(), buf.len() as CFIndex, K_CF_STRING_ENCODING_UTF8, ) }; if ok != 0 { // Find the NUL terminator. let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); String::from_utf8_lossy(&buf[..nul_pos]).into_owned() } else { String::new() } } } impl Clone for CfString { fn clone(&self) -> CfString { unsafe { cf_retain(self.as_void_ptr()) }; CfString(self.0) } } impl Drop for CfString { fn drop(&mut self) { unsafe { cf_release(self.as_void_ptr()) }; } } impl fmt::Debug for CfString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "CfString(\"{}\")", self.to_string_lossy()) } } impl fmt::Display for CfString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_string_lossy()) } } // SAFETY: CFString is an immutable, reference-counted CoreFoundation type. // CFRetain/CFRelease are thread-safe. unsafe impl Send for CfString {} unsafe impl Sync for CfString {} // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn create_cfstring_from_str() { let s = CfString::new("Hello, world!").expect("should create CFString"); assert_eq!(s.to_string_lossy(), "Hello, world!"); } #[test] fn cfstring_length() { let s = CfString::new("abc").expect("should create CFString"); assert_eq!(s.len(), 3); } #[test] fn cfstring_empty() { let s = CfString::new("").expect("should create empty CFString"); assert!(s.is_empty()); assert_eq!(s.len(), 0); assert_eq!(s.to_string_lossy(), ""); } #[test] fn cfstring_unicode() { let s = CfString::new("caf\u{00e9}").expect("should handle unicode"); assert_eq!(s.to_string_lossy(), "caf\u{00e9}"); // "caf\u{e9}" is 4 UTF-16 code units assert_eq!(s.len(), 4); } #[test] fn cfstring_emoji() { // Emoji outside BMP: encoded as surrogate pair in UTF-16 (2 code units) let s = CfString::new("\u{1F600}").expect("should handle emoji"); assert_eq!(s.to_string_lossy(), "\u{1F600}"); assert_eq!(s.len(), 2); // surrogate pair } #[test] fn cfstring_clone_and_drop() { let s = CfString::new("test clone").expect("should create"); let s2 = s.clone(); assert_eq!(s.to_string_lossy(), s2.to_string_lossy()); // Both should be droppable without crash (refcount was incremented). drop(s); assert_eq!(s2.to_string_lossy(), "test clone"); } #[test] fn cfstring_debug_format() { let s = CfString::new("debug").expect("should create"); let debug = format!("{:?}", s); assert!(debug.contains("CfString")); assert!(debug.contains("debug")); } #[test] fn cfstring_display_format() { let s = CfString::new("display").expect("should create"); let display = format!("{}", s); assert_eq!(display, "display"); } #[test] fn cfstring_as_cf_ref_not_null() { let s = CfString::new("ptr test").expect("should create"); assert!(!s.as_cf_ref().is_null()); } #[test] fn cfstring_as_void_ptr_not_null() { let s = CfString::new("void test").expect("should create"); assert!(!s.as_void_ptr().is_null()); } }