use core::ffi::CStr; use std::sync::OnceLock; use crate::ffi::NSInteger; use crate::rc::{autoreleasepool, Retained}; use crate::runtime::{AnyClass, NSObject}; use crate::{msg_send, ClassType}; // Marked `#[cold]` to tell the optimizer that errors are comparatively rare. // // And intentionally not `#[inline]`, we'll let the optimizer figure out if it // wants to do that or not. #[cold] pub(crate) unsafe fn encountered_error(err: *mut E) -> Retained { // SAFETY: Caller ensures that the pointer is valid. unsafe { Retained::retain(err) }.unwrap_or_else(|| { let err = null_error(); assert!(E::IS_NSERROR_COMPATIBLE); // SAFETY: Just checked (via `const` assertion) that the `E` type is // either `NSError` or `NSObject`, and hence it is valid to cast the // `NSObject` that we have here to that. unsafe { Retained::cast_unchecked(err) } }) } /// Poor mans string equality in `const`. Implements `a == b`. const fn is_eq(a: &str, b: &str) -> bool { let a = a.as_bytes(); let b = b.as_bytes(); if a.len() != b.len() { return false; } let mut i = 0; while i < a.len() { if a[i] != b[i] { return false; } i += 1; } true } // TODO: Use inline `const` once in MSRV (or add proper trait bounds). trait IsNSError { const IS_NSERROR_COMPATIBLE: bool; } impl IsNSError for T { const IS_NSERROR_COMPATIBLE: bool = { if is_eq(T::NAME, "NSError") || is_eq(T::NAME, "NSObject") { true } else { // The post monomorphization error here is not nice, but it's // better than UB because the user used a type that cannot be // converted to NSError. // // TODO: Add a trait bound or similar instead. panic!("error parameter must be either `NSError` or `NSObject`") } }; } #[cold] // Mark the NULL error branch as cold fn null_error() -> Retained { static CACHED_NULL_ERROR: OnceLock = OnceLock::new(); // We use a OnceLock here, since performance doesn't really matter, and // using an AtomicPtr would leak under (very) high initialization // contention. CACHED_NULL_ERROR.get_or_init(create_null_error).0.clone() } struct NSErrorWrapper(Retained); // SAFETY: NSError is immutable and thread safe. unsafe impl Send for NSErrorWrapper {} unsafe impl Sync for NSErrorWrapper {} #[cold] // Mark the error creation branch as cold fn create_null_error() -> NSErrorWrapper { // Wrap creation in an autoreleasepool, since we don't know anything about // the outside world, and we don't want to appear to leak. autoreleasepool(|_| { // TODO: Replace with c string literals once in MSRV. // SAFETY: The string is NUL terminated. let cls = unsafe { CStr::from_bytes_with_nul_unchecked(b"NSString\0") }; // Intentional dynamic lookup, we don't know if Foundation is linked. let cls = AnyClass::get(cls).unwrap_or_else(foundation_not_linked); // SAFETY: The string is NUL terminated. let domain = unsafe { CStr::from_bytes_with_nul_unchecked(b"__objc2.missingError\0") }; // SAFETY: The signate is correct, and the string is UTF-8 encoded and // NUL terminated. let domain: Retained = unsafe { msg_send![cls, stringWithUTF8String: domain.as_ptr()] }; // SAFETY: The string is valid. let cls = unsafe { CStr::from_bytes_with_nul_unchecked(b"NSError\0") }; // Intentional dynamic lookup, we don't know if Foundation is linked. let cls = AnyClass::get(cls).unwrap_or_else(foundation_not_linked); let domain: &NSObject = &domain; let code: NSInteger = 0; let user_info: Option<&NSObject> = None; // SAFETY: The signate is correct. let err: Retained = unsafe { msg_send![cls, errorWithDomain: domain, code: code, userInfo: user_info] }; NSErrorWrapper(err) }) } fn foundation_not_linked() -> &'static AnyClass { panic!("Foundation must be linked to get a proper error message on NULL errors") } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_eq() { assert!(is_eq("NSError", "NSError")); assert!(!is_eq("nserror", "NSError")); assert!(!is_eq("CFError", "NSError")); assert!(!is_eq("NSErr", "NSError")); assert!(!is_eq("NSErrorrrr", "NSError")); } #[test] fn test_create() { let _ = create_null_error().0; } }