web engine - experimental web browser

Merge branch 'objc-ffi': Objective-C runtime FFI

Implements issue 3mfkgyov6gf2k: Raw extern "C" bindings to libobjc.dylib,
Sel/Class/Id wrappers, msg_send!/sel!/class! macros, and 12 unit tests.

+585
+2
crates/platform/src/lib.rs
··· 1 1 //! Minimal macOS platform layer — Obj-C FFI, AppKit, CoreGraphics, Metal. 2 + 3 + pub mod objc;
+583
crates/platform/src/objc.rs
··· 1 + //! Objective-C runtime FFI bindings and Rust abstractions. 2 + //! 3 + //! Provides safe(ish) wrappers around the Objective-C runtime for message 4 + //! dispatch, class lookup, selector registration, and dynamic class creation. 5 + //! 6 + //! # Safety 7 + //! 8 + //! This module contains `unsafe` code for FFI with `libobjc.dylib`. 9 + //! The `platform` crate is one of the few crates where `unsafe` is permitted. 10 + 11 + use std::ffi::CStr; 12 + use std::fmt; 13 + use std::os::raw::c_char; 14 + use std::ptr::NonNull; 15 + 16 + // --------------------------------------------------------------------------- 17 + // Raw FFI bindings to libobjc.dylib 18 + // --------------------------------------------------------------------------- 19 + 20 + // Opaque types matching the ObjC runtime's internal structures. 21 + // These are public because they appear in the signatures of public methods 22 + // (e.g. `as_ptr`), but they are zero-sized and cannot be constructed. 23 + #[repr(C)] 24 + pub struct ObjcClass { 25 + _private: [u8; 0], 26 + } 27 + 28 + #[repr(C)] 29 + pub struct ObjcObject { 30 + _private: [u8; 0], 31 + } 32 + 33 + #[repr(C)] 34 + pub struct ObjcSelector { 35 + _private: [u8; 0], 36 + } 37 + 38 + /// An Objective-C method implementation function pointer. 39 + /// 40 + /// The actual signature varies per method, but the runtime treats all IMPs 41 + /// as `extern "C" fn()` for registration purposes. 42 + pub type Imp = unsafe extern "C" fn(); 43 + 44 + #[link(name = "objc", kind = "dylib")] 45 + extern "C" { 46 + fn objc_getClass(name: *const c_char) -> *mut ObjcClass; 47 + fn sel_registerName(name: *const c_char) -> *mut ObjcSelector; 48 + fn objc_allocateClassPair( 49 + superclass: *mut ObjcClass, 50 + name: *const c_char, 51 + extra_bytes: usize, 52 + ) -> *mut ObjcClass; 53 + fn objc_registerClassPair(cls: *mut ObjcClass); 54 + fn class_addMethod( 55 + cls: *mut ObjcClass, 56 + sel: *mut ObjcSelector, 57 + imp: Imp, 58 + types: *const c_char, 59 + ) -> bool; 60 + fn class_getName(cls: *mut ObjcClass) -> *const c_char; 61 + 62 + // objc_msgSend has a variadic ABI. On AArch64 the calling convention is 63 + // the standard C calling convention (arguments in x0..x7, return in x0). 64 + // We declare it with no extra arguments; the msg_send! macro transmutes 65 + // the function pointer to the appropriate concrete signature at each call 66 + // site. 67 + fn objc_msgSend(); 68 + } 69 + 70 + // --------------------------------------------------------------------------- 71 + // Sel — a registered selector 72 + // --------------------------------------------------------------------------- 73 + 74 + /// A registered Objective-C selector. 75 + /// 76 + /// Selectors are interned strings that identify a method name. Two selectors 77 + /// with the same name are pointer-equal. 78 + #[derive(Clone, Copy, PartialEq, Eq, Hash)] 79 + #[repr(transparent)] 80 + pub struct Sel(NonNull<ObjcSelector>); 81 + 82 + impl Sel { 83 + /// Register (or look up) a selector by name. 84 + /// 85 + /// `name` must be a nul-terminated C string (use `c"foo:"` literals or 86 + /// the `sel!` helper). 87 + /// 88 + /// # Panics 89 + /// 90 + /// Panics if the runtime returns null (should never happen for valid names). 91 + pub fn register(name: &CStr) -> Sel { 92 + // SAFETY: sel_registerName is thread-safe and always returns a valid 93 + // pointer for well-formed selector names. 94 + let ptr = unsafe { sel_registerName(name.as_ptr()) }; 95 + Sel(NonNull::new(ptr).expect("sel_registerName returned null")) 96 + } 97 + 98 + /// Return the raw pointer to the selector. 99 + #[inline] 100 + pub fn as_ptr(self) -> *mut ObjcSelector { 101 + self.0.as_ptr() 102 + } 103 + } 104 + 105 + impl fmt::Debug for Sel { 106 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 + // We can't easily get the selector name without sel_getName, 108 + // so just print the pointer. 109 + write!(f, "Sel({:?})", self.0) 110 + } 111 + } 112 + 113 + // SAFETY: Selectors are globally interned pointers; they're safe to send 114 + // across threads. 115 + unsafe impl Send for Sel {} 116 + unsafe impl Sync for Sel {} 117 + 118 + // --------------------------------------------------------------------------- 119 + // Class — a reference to an Obj-C class 120 + // --------------------------------------------------------------------------- 121 + 122 + /// A reference to an Objective-C class. 123 + #[derive(Clone, Copy, PartialEq, Eq, Hash)] 124 + #[repr(transparent)] 125 + pub struct Class(NonNull<ObjcClass>); 126 + 127 + impl Class { 128 + /// Look up a class by name. 129 + /// 130 + /// Returns `None` if no class with that name is registered. 131 + pub fn get(name: &CStr) -> Option<Class> { 132 + // SAFETY: objc_getClass is thread-safe; it returns null for unknown 133 + // class names. 134 + let ptr = unsafe { objc_getClass(name.as_ptr()) }; 135 + NonNull::new(ptr).map(Class) 136 + } 137 + 138 + /// Allocate a new class pair (a new class and its metaclass). 139 + /// 140 + /// `superclass` is the parent class; `name` is the new class name; 141 + /// `extra_bytes` is usually 0. 142 + /// 143 + /// Returns `None` if a class with `name` already exists. 144 + /// 145 + /// You must call [`Class::register`] on the returned class after adding 146 + /// methods and ivars. 147 + pub fn allocate(superclass: Class, name: &CStr, extra_bytes: usize) -> Option<Class> { 148 + // SAFETY: objc_allocateClassPair returns null if the name is already 149 + // taken. The superclass pointer is valid because it came from a Class. 150 + let ptr = 151 + unsafe { objc_allocateClassPair(superclass.as_ptr(), name.as_ptr(), extra_bytes) }; 152 + NonNull::new(ptr).map(Class) 153 + } 154 + 155 + /// Register a class that was previously created with [`Class::allocate`]. 156 + /// 157 + /// After this call the class is usable and no further methods/ivars can be 158 + /// added. 159 + pub fn register(self) { 160 + // SAFETY: the pointer is valid and the class has not been registered 161 + // yet (caller invariant). 162 + unsafe { objc_registerClassPair(self.as_ptr()) } 163 + } 164 + 165 + /// Add a method to a class that has not yet been registered. 166 + /// 167 + /// `sel` is the method selector, `imp` is the implementation function 168 + /// pointer, and `types` is the ObjC type encoding string. 169 + /// 170 + /// Returns `true` if the method was added, `false` if a method with that 171 + /// selector already existed. 172 + pub fn add_method(self, sel: Sel, imp: Imp, types: &CStr) -> bool { 173 + // SAFETY: the class pointer is valid and not yet registered. 174 + unsafe { class_addMethod(self.as_ptr(), sel.as_ptr(), imp, types.as_ptr()) } 175 + } 176 + 177 + /// Get the name of this class. 178 + pub fn name(self) -> &'static str { 179 + // SAFETY: class_getName always returns a valid C string for a valid 180 + // class pointer. 181 + let c_str = unsafe { CStr::from_ptr(class_getName(self.as_ptr())) }; 182 + c_str.to_str().unwrap_or("<invalid UTF-8>") 183 + } 184 + 185 + /// Return the raw pointer to the class. 186 + #[inline] 187 + pub fn as_ptr(self) -> *mut ObjcClass { 188 + self.0.as_ptr() 189 + } 190 + } 191 + 192 + impl fmt::Debug for Class { 193 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 194 + write!(f, "Class(\"{}\")", self.name()) 195 + } 196 + } 197 + 198 + // SAFETY: ObjC classes are globally registered and immutable after 199 + // registration; safe to share across threads. 200 + unsafe impl Send for Class {} 201 + unsafe impl Sync for Class {} 202 + 203 + // --------------------------------------------------------------------------- 204 + // Id — a non-null pointer to an Obj-C object instance 205 + // --------------------------------------------------------------------------- 206 + 207 + /// A non-null pointer to an Objective-C object. 208 + /// 209 + /// This is an *unowned* (raw) reference — no automatic retain/release. 210 + /// The caller is responsible for memory management. 211 + #[derive(Clone, Copy, PartialEq, Eq, Hash)] 212 + #[repr(transparent)] 213 + pub struct Id(NonNull<ObjcObject>); 214 + 215 + impl Id { 216 + /// Create an `Id` from a raw non-null pointer. 217 + /// 218 + /// # Safety 219 + /// 220 + /// The pointer must point to a valid Objective-C object. 221 + #[inline] 222 + pub unsafe fn from_ptr(ptr: NonNull<ObjcObject>) -> Id { 223 + Id(ptr) 224 + } 225 + 226 + /// Try to create an `Id` from a raw pointer that might be null. 227 + /// 228 + /// # Safety 229 + /// 230 + /// If non-null, the pointer must point to a valid Objective-C object. 231 + #[inline] 232 + pub unsafe fn from_raw(ptr: *mut ObjcObject) -> Option<Id> { 233 + NonNull::new(ptr).map(Id) 234 + } 235 + 236 + /// Return the raw pointer. 237 + #[inline] 238 + pub fn as_ptr(self) -> *mut ObjcObject { 239 + self.0.as_ptr() 240 + } 241 + 242 + /// Cast to `*mut T` for use as a function argument. 243 + /// 244 + /// # Safety 245 + /// 246 + /// The caller must ensure the object is actually of type `T`. 247 + #[inline] 248 + pub unsafe fn cast<T>(self) -> *mut T { 249 + self.0.as_ptr().cast() 250 + } 251 + } 252 + 253 + impl fmt::Debug for Id { 254 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 255 + write!(f, "Id({:?})", self.0) 256 + } 257 + } 258 + 259 + // SAFETY: ObjC objects can be sent across threads (whether this is safe 260 + // depends on the object, but at the pointer level it's fine). 261 + unsafe impl Send for Id {} 262 + unsafe impl Sync for Id {} 263 + 264 + // --------------------------------------------------------------------------- 265 + // msg_send! macro 266 + // --------------------------------------------------------------------------- 267 + 268 + /// Return the raw `objc_msgSend` function pointer. 269 + /// 270 + /// The caller must transmute this to the correct function signature for the 271 + /// message being sent. The `msg_send!` macro does this automatically. 272 + #[inline] 273 + pub fn msg_send_fn() -> unsafe extern "C" fn() { 274 + objc_msgSend 275 + } 276 + 277 + /// Send an Objective-C message. 278 + /// 279 + /// # Syntax 280 + /// 281 + /// ```ignore 282 + /// // No arguments, returns Id: 283 + /// let obj: Id = msg_send![class.as_ptr(), alloc]; 284 + /// 285 + /// // With arguments, returns Id: 286 + /// let obj: Id = msg_send![obj.as_ptr(), initWithFrame: rect]; 287 + /// 288 + /// // No return value (returns ()): 289 + /// msg_send![obj.as_ptr(), release]; 290 + /// ``` 291 + /// 292 + /// The first argument must be a `*mut T` (the receiver). The return type is 293 + /// inferred from context. 294 + /// 295 + /// # Safety 296 + /// 297 + /// This is inherently unsafe: wrong selector, wrong argument types, or wrong 298 + /// return type will cause undefined behavior. 299 + #[macro_export] 300 + macro_rules! msg_send { 301 + // No arguments: msg_send![receiver, selector] 302 + [$receiver:expr, $sel:ident] => {{ 303 + let sel_name = concat!(stringify!($sel), "\0"); 304 + let sel = $crate::objc::Sel::register(unsafe { 305 + std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) 306 + }); 307 + let func: unsafe extern "C" fn(*mut std::os::raw::c_void, *mut std::os::raw::c_void) -> _ 308 + = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 309 + unsafe { func($receiver as *mut std::os::raw::c_void, sel.as_ptr() as *mut std::os::raw::c_void) } 310 + }}; 311 + 312 + // One argument: msg_send![receiver, selector: arg] 313 + [$receiver:expr, $sel:ident : $arg:expr] => {{ 314 + let sel_name = concat!(stringify!($sel), ":\0"); 315 + let sel = $crate::objc::Sel::register(unsafe { 316 + std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) 317 + }); 318 + let func: unsafe extern "C" fn( 319 + *mut std::os::raw::c_void, 320 + *mut std::os::raw::c_void, 321 + _, 322 + ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 323 + unsafe { 324 + func( 325 + $receiver as *mut std::os::raw::c_void, 326 + sel.as_ptr() as *mut std::os::raw::c_void, 327 + $arg, 328 + ) 329 + } 330 + }}; 331 + 332 + // Two arguments: msg_send![receiver, sel1: arg1, sel2: arg2] 333 + [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr] => {{ 334 + let sel_name = concat!(stringify!($sel1), ":", stringify!($sel2), ":\0"); 335 + let sel = $crate::objc::Sel::register(unsafe { 336 + std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) 337 + }); 338 + let func: unsafe extern "C" fn( 339 + *mut std::os::raw::c_void, 340 + *mut std::os::raw::c_void, 341 + _, 342 + _, 343 + ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 344 + unsafe { 345 + func( 346 + $receiver as *mut std::os::raw::c_void, 347 + sel.as_ptr() as *mut std::os::raw::c_void, 348 + $arg1, 349 + $arg2, 350 + ) 351 + } 352 + }}; 353 + 354 + // Three arguments: msg_send![receiver, sel1: arg1, sel2: arg2, sel3: arg3] 355 + [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr, $sel3:ident : $arg3:expr] => {{ 356 + let sel_name = concat!( 357 + stringify!($sel1), ":", 358 + stringify!($sel2), ":", 359 + stringify!($sel3), ":\0" 360 + ); 361 + let sel = $crate::objc::Sel::register(unsafe { 362 + std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) 363 + }); 364 + let func: unsafe extern "C" fn( 365 + *mut std::os::raw::c_void, 366 + *mut std::os::raw::c_void, 367 + _, 368 + _, 369 + _, 370 + ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 371 + unsafe { 372 + func( 373 + $receiver as *mut std::os::raw::c_void, 374 + sel.as_ptr() as *mut std::os::raw::c_void, 375 + $arg1, 376 + $arg2, 377 + $arg3, 378 + ) 379 + } 380 + }}; 381 + 382 + // Four arguments 383 + [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr, $sel3:ident : $arg3:expr, $sel4:ident : $arg4:expr] => {{ 384 + let sel_name = concat!( 385 + stringify!($sel1), ":", 386 + stringify!($sel2), ":", 387 + stringify!($sel3), ":", 388 + stringify!($sel4), ":\0" 389 + ); 390 + let sel = $crate::objc::Sel::register(unsafe { 391 + std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) 392 + }); 393 + let func: unsafe extern "C" fn( 394 + *mut std::os::raw::c_void, 395 + *mut std::os::raw::c_void, 396 + _, 397 + _, 398 + _, 399 + _, 400 + ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 401 + unsafe { 402 + func( 403 + $receiver as *mut std::os::raw::c_void, 404 + sel.as_ptr() as *mut std::os::raw::c_void, 405 + $arg1, 406 + $arg2, 407 + $arg3, 408 + $arg4, 409 + ) 410 + } 411 + }}; 412 + } 413 + 414 + // --------------------------------------------------------------------------- 415 + // Convenience: sel! macro 416 + // --------------------------------------------------------------------------- 417 + 418 + /// Register a selector from a string literal. 419 + /// 420 + /// ```ignore 421 + /// let sel = sel!("init"); 422 + /// let sel = sel!("initWithFrame:"); 423 + /// ``` 424 + #[macro_export] 425 + macro_rules! sel { 426 + ($name:literal) => {{ 427 + $crate::objc::Sel::register(unsafe { 428 + std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($name, "\0").as_bytes()) 429 + }) 430 + }}; 431 + } 432 + 433 + /// Look up a class by name (string literal). 434 + /// 435 + /// Returns `Option<Class>`. 436 + /// 437 + /// ```ignore 438 + /// let cls = class!("NSObject").unwrap(); 439 + /// ``` 440 + #[macro_export] 441 + macro_rules! class { 442 + ($name:literal) => {{ 443 + $crate::objc::Class::get(unsafe { 444 + std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($name, "\0").as_bytes()) 445 + }) 446 + }}; 447 + } 448 + 449 + // --------------------------------------------------------------------------- 450 + // Tests 451 + // --------------------------------------------------------------------------- 452 + 453 + #[cfg(test)] 454 + mod tests { 455 + use super::*; 456 + 457 + #[test] 458 + fn get_nsobject_class() { 459 + let cls = Class::get(c"NSObject").expect("NSObject class should exist"); 460 + assert_eq!(cls.name(), "NSObject"); 461 + } 462 + 463 + #[test] 464 + fn get_nonexistent_class_returns_none() { 465 + let cls = Class::get(c"ThisClassDoesNotExist12345"); 466 + assert!(cls.is_none()); 467 + } 468 + 469 + #[test] 470 + fn register_selector() { 471 + let sel1 = Sel::register(c"init"); 472 + let sel2 = Sel::register(c"init"); 473 + // Same selector name should return the same pointer. 474 + assert_eq!(sel1, sel2); 475 + } 476 + 477 + #[test] 478 + fn different_selectors_differ() { 479 + let sel1 = Sel::register(c"init"); 480 + let sel2 = Sel::register(c"alloc"); 481 + assert_ne!(sel1, sel2); 482 + } 483 + 484 + #[test] 485 + fn alloc_init_nsobject() { 486 + let cls = Class::get(c"NSObject").unwrap(); 487 + // alloc 488 + let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; 489 + assert!(!obj.is_null()); 490 + // init 491 + let obj: *mut std::os::raw::c_void = msg_send![obj, init]; 492 + assert!(!obj.is_null()); 493 + // release 494 + let _: *mut std::os::raw::c_void = msg_send![obj, release]; 495 + } 496 + 497 + #[test] 498 + fn class_macro_works() { 499 + let cls = class!("NSObject").expect("NSObject should exist"); 500 + assert_eq!(cls.name(), "NSObject"); 501 + } 502 + 503 + #[test] 504 + fn sel_macro_works() { 505 + let sel = sel!("init"); 506 + let sel2 = Sel::register(c"init"); 507 + assert_eq!(sel, sel2); 508 + } 509 + 510 + #[test] 511 + fn allocate_and_register_custom_class() { 512 + let superclass = Class::get(c"NSObject").unwrap(); 513 + // Use a unique class name to avoid conflicts with other tests. 514 + let new_cls = Class::allocate(superclass, c"WeTestCustomClass", 0) 515 + .expect("should allocate new class"); 516 + new_cls.register(); 517 + // Verify we can look it up. 518 + let found = Class::get(c"WeTestCustomClass").expect("custom class should be registered"); 519 + assert_eq!(found.name(), "WeTestCustomClass"); 520 + } 521 + 522 + #[test] 523 + fn add_method_to_custom_class() { 524 + let superclass = Class::get(c"NSObject").unwrap(); 525 + let new_cls = Class::allocate(superclass, c"WeTestMethodClass", 0) 526 + .expect("should allocate new class"); 527 + 528 + // Define a simple method that returns an integer. 529 + extern "C" fn my_method( 530 + _this: *mut std::os::raw::c_void, 531 + _sel: *mut std::os::raw::c_void, 532 + ) -> i64 { 533 + 42 534 + } 535 + 536 + let sel = Sel::register(c"myMethod"); 537 + // Type encoding: returns long long (q), takes id (@) and SEL (:) 538 + let added = new_cls.add_method( 539 + sel, 540 + unsafe { std::mem::transmute::<*const (), Imp>(my_method as *const ()) }, 541 + c"q@:", 542 + ); 543 + assert!(added); 544 + 545 + new_cls.register(); 546 + 547 + // Allocate an instance and call our method. 548 + let cls = Class::get(c"WeTestMethodClass").unwrap(); 549 + let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; 550 + let obj: *mut std::os::raw::c_void = msg_send![obj, init]; 551 + let result: i64 = msg_send![obj, myMethod]; 552 + assert_eq!(result, 42); 553 + let _: *mut std::os::raw::c_void = msg_send![obj, release]; 554 + } 555 + 556 + #[test] 557 + fn msg_send_with_argument() { 558 + let cls = Class::get(c"NSObject").unwrap(); 559 + let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; 560 + let obj: *mut std::os::raw::c_void = msg_send![obj, init]; 561 + 562 + // respondsToSelector: takes a SEL, returns BOOL (i8 on AArch64). 563 + let sel = Sel::register(c"init"); 564 + let responds: bool = msg_send![obj, respondsToSelector: sel.as_ptr()]; 565 + assert!(responds); 566 + 567 + let _: *mut std::os::raw::c_void = msg_send![obj, release]; 568 + } 569 + 570 + #[test] 571 + fn class_debug_format() { 572 + let cls = Class::get(c"NSObject").unwrap(); 573 + let debug = format!("{:?}", cls); 574 + assert!(debug.contains("NSObject")); 575 + } 576 + 577 + #[test] 578 + fn sel_debug_format() { 579 + let sel = Sel::register(c"init"); 580 + let debug = format!("{:?}", sel); 581 + assert!(debug.contains("Sel(")); 582 + } 583 + }