diff --git a/Cargo.lock b/Cargo.lock index 4070e5d23..5126399ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1387,6 +1387,13 @@ dependencies = [ "objc2", ] +[[package]] +name = "test_available" +version = "0.1.0" +dependencies = [ + "objc2", +] + [[package]] name = "test_block" version = "0.1.0" diff --git a/crates/objc2/CHANGELOG.md b/crates/objc2/CHANGELOG.md index 0cda0214e..61724a1ef 100644 --- a/crates/objc2/CHANGELOG.md +++ b/crates/objc2/CHANGELOG.md @@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Allow using `Into` to convert to retained objects. * Make `Retained::into_super` an inherent method instead of an associated method. This means that you can now use it as `.into_super()`. +* Added the `available!()` macro for determining whether code is running on + a given operating system. ### Changed * **BREAKING**: Changed how you specify a class to only be available on the diff --git a/crates/objc2/src/__macro_helpers/mod.rs b/crates/objc2/src/__macro_helpers/mod.rs index 51562c19c..8156def08 100644 --- a/crates/objc2/src/__macro_helpers/mod.rs +++ b/crates/objc2/src/__macro_helpers/mod.rs @@ -1,6 +1,7 @@ pub use core::borrow::Borrow; pub use core::cell::UnsafeCell; pub use core::convert::AsRef; +pub use core::default::Default; pub use core::marker::{PhantomData, Sized}; pub use core::mem::{size_of, ManuallyDrop, MaybeUninit}; pub use core::ops::Deref; @@ -21,6 +22,7 @@ mod method_family; mod module_info; mod msg_send; mod msg_send_retained; +mod os_version; mod writeback; pub use self::cache::{CachedClass, CachedSel}; @@ -38,6 +40,7 @@ pub use self::method_family::{ pub use self::module_info::ModuleInfo; pub use self::msg_send::MsgSend; pub use self::msg_send_retained::{MaybeUnwrap, MsgSendId, MsgSendSuperId}; +pub use self::os_version::{is_available, AvailableVersion, OSVersion}; /// Disallow using this passed in value in const and statics for forwards /// compatibility (this function is not a `const` function). diff --git a/crates/objc2/src/__macro_helpers/os_version.rs b/crates/objc2/src/__macro_helpers/os_version.rs new file mode 100644 index 000000000..fa7e6474b --- /dev/null +++ b/crates/objc2/src/__macro_helpers/os_version.rs @@ -0,0 +1,429 @@ +//! Utilities for checking the runtime availability of APIs. +//! +//! TODO: Upstream some of this to `std`? +use core::cmp::Ordering; +use core::fmt; + +#[cfg(target_vendor = "apple")] +mod apple; + +/// The size of the fields here are limited by Mach-O's `LC_BUILD_VERSION`. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OSVersion { + // Shuffle the versions around a little so that OSVersion has the same bit + // representation as the `u32` returned from `to_u32`, allowing + // comparisons to compile down to just between two `u32`s. + #[cfg(target_endian = "little")] + pub patch: u8, + #[cfg(target_endian = "little")] + pub minor: u8, + #[cfg(target_endian = "little")] + pub major: u16, + + #[cfg(target_endian = "big")] + pub major: u16, + #[cfg(target_endian = "big")] + pub minor: u8, + #[cfg(target_endian = "big")] + pub patch: u8, +} + +#[track_caller] +const fn parse_usize(mut bytes: &[u8]) -> (usize, &[u8]) { + // Ensure we have at least one digit (that is not just a period). + let mut ret: usize = if let Some((&ascii, rest)) = bytes.split_first() { + bytes = rest; + + match ascii { + b'0'..=b'9' => (ascii - b'0') as usize, + _ => panic!("found invalid digit when parsing version"), + } + } else { + panic!("found empty version number part") + }; + + // Parse the remaining digits. + while let Some((&ascii, rest)) = bytes.split_first() { + let digit = match ascii { + b'0'..=b'9' => ascii - b'0', + _ => break, + }; + + bytes = rest; + + // This handles leading zeroes as well. + match ret.checked_mul(10) { + Some(val) => match val.checked_add(digit as _) { + Some(val) => ret = val, + None => panic!("version is too large"), + }, + None => panic!("version is too large"), + }; + } + + (ret, bytes) +} + +impl OSVersion { + const MIN: Self = Self { + major: 0, + minor: 0, + patch: 0, + }; + + const MAX: Self = Self { + major: u16::MAX, + minor: u8::MAX, + patch: u8::MAX, + }; + + /// Parse the version from a string at `const` time. + #[track_caller] + pub const fn from_str(version: &str) -> Self { + Self::from_bytes(version.as_bytes()) + } + + #[track_caller] + pub(crate) const fn from_bytes(bytes: &[u8]) -> Self { + let (major, bytes) = parse_usize(bytes); + if major > u16::MAX as usize { + panic!("major version is too large"); + } + let major = major as u16; + + let bytes = if let Some((period, bytes)) = bytes.split_first() { + if *period != b'.' { + panic!("expected period between major and minor version") + } + bytes + } else { + return Self { + major, + minor: 0, + patch: 0, + }; + }; + + let (minor, bytes) = parse_usize(bytes); + if minor > u8::MAX as usize { + panic!("minor version is too large"); + } + let minor = minor as u8; + + let bytes = if let Some((period, bytes)) = bytes.split_first() { + if *period != b'.' { + panic!("expected period after minor version") + } + bytes + } else { + return Self { + major, + minor, + patch: 0, + }; + }; + + let (patch, bytes) = parse_usize(bytes); + if patch > u8::MAX as usize { + panic!("patch version is too large"); + } + let patch = patch as u8; + + if !bytes.is_empty() { + panic!("too many parts to version"); + } + + Self { + major, + minor, + patch, + } + } + + /// Pack the version into a `u32`. + /// + /// This is used for faster comparisons. + #[inline] + pub const fn to_u32(self) -> u32 { + // See comments in `OSVersion`, this should compile down to nothing. + let (major, minor, patch) = (self.major as u32, self.minor as u32, self.patch as u32); + (major << 16) | (minor << 8) | patch + } + + /// Construct the version from a `u32`. + #[inline] + pub const fn from_u32(version: u32) -> Self { + // See comments in `OSVersion`, this should compile down to nothing. + let major = (version >> 16) as u16; + let minor = (version >> 8) as u8; + let patch = version as u8; + Self { + major, + minor, + patch, + } + } +} + +impl PartialEq for OSVersion { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.to_u32() == other.to_u32() + } +} + +impl PartialOrd for OSVersion { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.to_u32().partial_cmp(&other.to_u32()) + } +} + +impl fmt::Debug for OSVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Same ordering on little and big endian. + f.debug_struct("OSVersion") + .field("major", &self.major) + .field("minor", &self.minor) + .field("patch", &self.patch) + .finish() + } +} + +/// The combined availability. +/// +/// This generally works closely together with the `available!` macro to make +/// syntax checking inside that easier. +#[derive(Clone, Copy, Debug)] +pub struct AvailableVersion { + pub macos: OSVersion, + pub ios: OSVersion, + pub tvos: OSVersion, + pub watchos: OSVersion, + pub visionos: OSVersion, + #[doc(hidden)] + pub __others: OSVersion, +} + +impl AvailableVersion { + pub const MIN: Self = Self { + macos: OSVersion::MIN, + ios: OSVersion::MIN, + tvos: OSVersion::MIN, + watchos: OSVersion::MIN, + visionos: OSVersion::MIN, + __others: OSVersion::MIN, + }; + + pub const MAX: Self = Self { + macos: OSVersion::MAX, + ios: OSVersion::MAX, + tvos: OSVersion::MAX, + watchos: OSVersion::MAX, + visionos: OSVersion::MAX, + __others: OSVersion::MAX, + }; +} + +#[inline] +pub fn is_available(version: AvailableVersion) -> bool { + let version = if cfg!(target_os = "macos") { + version.macos + } else if cfg!(target_os = "ios") { + version.ios + } else if cfg!(target_os = "tvos") { + version.tvos + } else if cfg!(target_os = "watchos") { + version.watchos + } else if cfg!(target_os = "visionos") { + version.visionos + } else { + version.__others + }; + + // In the special case that `version` was set to `OSVersion::MAX`, we + // assume that there can never be an OS version that large, and hence we + // want to avoid checking at all. + // + // This is useful for platforms where the version hasn't been specified. + if version == OSVersion::MAX { + return false; + } + + #[cfg(target_vendor = "apple")] + { + // If the deployment target is high enough, the API is always available. + // + // This check should be optimized away at compile time. + if version <= apple::DEPLOYMENT_TARGET { + return true; + } + + // Otherwise, compare against the version at runtime. + version <= apple::current_version() + } + + #[cfg(not(target_vendor = "apple"))] + return true; +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{__available_version, available}; + + #[test] + fn test_parse() { + #[track_caller] + fn check(expected: (u16, u8, u8), actual: OSVersion) { + assert_eq!( + OSVersion { + major: expected.0, + minor: expected.1, + patch: expected.2, + }, + actual, + ) + } + + check((1, 0, 0), __available_version!(1)); + check((1, 2, 0), __available_version!(1.2)); + check((1, 2, 3), __available_version!(1.2.3)); + check((9999, 99, 99), __available_version!(9999.99.99)); + + // Ensure that the macro handles leading zeroes correctly + check((10, 0, 0), __available_version!(010)); + check((10, 20, 0), __available_version!(010.020)); + check((10, 20, 30), __available_version!(010.020.030)); + check( + (10000, 100, 100), + __available_version!(000010000.00100.00100), + ); + } + + #[test] + fn test_compare() { + #[track_caller] + fn check_lt(expected: (u16, u8, u8), actual: (u16, u8, u8)) { + assert!( + OSVersion { + major: expected.0, + minor: expected.1, + patch: expected.2, + } < OSVersion { + major: actual.0, + minor: actual.1, + patch: actual.2, + }, + ) + } + + check_lt((4, 99, 99), (5, 5, 5)); + check_lt((5, 4, 99), (5, 5, 5)); + check_lt((5, 5, 4), (5, 5, 5)); + + check_lt((10, 7, 0), (10, 10, 0)); + } + + #[test] + #[should_panic = "too many parts to version"] + fn test_too_many_version_parts() { + let _ = __available_version!(1.2.3 .4); + } + + #[test] + #[should_panic = "found invalid digit when parsing version"] + fn test_macro_with_identifiers() { + let _ = __available_version!(A.B); + } + + #[test] + #[should_panic = "found empty version number part"] + fn test_empty_version() { + let _ = __available_version!(); + } + + #[test] + #[should_panic = "found invalid digit when parsing version"] + fn test_only_period() { + let _ = __available_version!(.); + } + + #[test] + #[should_panic = "found invalid digit when parsing version"] + fn test_has_leading_period() { + let _ = __available_version!(.1); + } + + #[test] + #[should_panic = "found empty version number part"] + fn test_has_trailing_period() { + let _ = __available_version!(1.); + } + + #[test] + #[should_panic = "major version is too large"] + fn test_major_too_large() { + let _ = __available_version!(100000); + } + + #[test] + #[should_panic = "minor version is too large"] + fn test_minor_too_large() { + let _ = __available_version!(1.1000); + } + + #[test] + #[should_panic = "patch version is too large"] + fn test_patch_too_large() { + let _ = __available_version!(1.1.1000); + } + + #[test] + fn test_general_available() { + // Always available + assert!(available!(..)); + + // Never available + assert!(!available!()); + + // Low versions, always available + assert!(available!( + macos = 10.0, + ios = 1.0, + tvos = 1.0, + watchos = 1.0, + visionos = 1.0, + .. + )); + + // High versions, never available + assert!(!available!( + macos = 99, + ios = 99, + tvos = 99, + watchos = 99, + visionos = 99 + )); + + if !cfg!(target_os = "tvos") { + // Available nowhere except tvOS + assert!(!available!(tvos = 1.2)); + + // Available everywhere, except low tvOS versions + assert!(available!(tvos = 1.2, ..)); + } + } + + #[test] + fn test_u32_roundtrip() { + let version = OSVersion { + major: 1000, + minor: 100, + patch: 200, + }; + assert_eq!(version, OSVersion::from_u32(version.to_u32())); + } +} diff --git a/crates/objc2/src/__macro_helpers/os_version/apple.rs b/crates/objc2/src/__macro_helpers/os_version/apple.rs new file mode 100644 index 000000000..ef88e7f23 --- /dev/null +++ b/crates/objc2/src/__macro_helpers/os_version/apple.rs @@ -0,0 +1,303 @@ +use core::ffi::{c_char, c_uint, c_void}; +use core::num::NonZeroU32; +use core::ptr; +use core::sync::atomic::{AtomicU32, Ordering}; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; + +use super::OSVersion; +use crate::rc::{autoreleasepool, Allocated, Retained}; +use crate::runtime::__nsstring::{nsstring_to_str, UTF8_ENCODING}; +use crate::runtime::{NSObject, NSObjectProtocol}; +use crate::{class, msg_send_id}; + +/// The deployment target for the current OS. +pub(crate) const DEPLOYMENT_TARGET: OSVersion = { + // Intentionally use `#[cfg]` guards instead of `cfg!` here, to avoid + // recompiling when unrelated environment variables change. + #[cfg(target_os = "macos")] + let var = option_env!("MACOSX_DEPLOYMENT_TARGET"); + #[cfg(target_os = "ios")] // Also used on Mac Catalyst. + let var = option_env!("IPHONEOS_DEPLOYMENT_TARGET"); + #[cfg(target_os = "tvos")] + let var = option_env!("TVOS_DEPLOYMENT_TARGET"); + #[cfg(target_os = "watchos")] + let var = option_env!("WATCHOS_DEPLOYMENT_TARGET"); + #[cfg(target_os = "visionos")] + let var = option_env!("XROS_DEPLOYMENT_TARGET"); + + if let Some(var) = var { + OSVersion::from_str(var) + } else { + // Default operating system version. + // See + // + // Note that we cannot do as they suggest, and use + // `rustc --print=deployment-target`, as this has to work at `const` + // time. + #[allow(clippy::if_same_then_else)] + let os_min = if cfg!(target_os = "macos") { + (10, 12, 0) + } else if cfg!(target_os = "ios") { + (10, 0, 0) + } else if cfg!(target_os = "tvos") { + (10, 0, 0) + } else if cfg!(target_os = "watchos") { + (5, 0, 0) + } else if cfg!(target_os = "visionos") { + (1, 0, 0) + } else { + panic!("unknown Apple OS") + }; + + // On certain targets it makes sense to raise the minimum OS version. + // + // See + // + // Note that we cannot do all the same checks as `rustc` does, because + // we have no way of knowing if the architecture is `arm64e` without + // reading the target triple itself (and we want to get rid of build + // scripts). + #[allow(clippy::if_same_then_else)] + let min = if cfg!(all(target_os = "macos", target_arch = "aarch64")) { + (11, 0, 0) + } else if cfg!(all( + target_os = "ios", + target_arch = "aarch64", + target_abi_macabi + )) { + (14, 0, 0) + } else if cfg!(all( + target_os = "ios", + target_arch = "aarch64", + target_simulator + )) { + (14, 0, 0) + } else if cfg!(all(target_os = "tvos", target_arch = "aarch64")) { + (14, 0, 0) + } else if cfg!(all(target_os = "watchos", target_arch = "aarch64")) { + (7, 0, 0) + } else { + os_min + }; + + OSVersion { + major: min.0, + minor: min.1, + patch: min.2, + } + } +}; + +/// Look up the current version at runtime. +/// +/// Note that this doesn't work with "zippered" `dylib`s yet, though +/// that's probably fine, `rustc` doesn't support those either: +/// +#[inline] +pub(crate) fn current_version() -> OSVersion { + // Cache the lookup for performance. + // + // We assume that 0.0.0 is never gonna be a valid version, + // and use that as our sentinel value. + static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0); + + // We use relaxed atomics, it doesn't matter if two threads end up racing + // to read or write the version. + let version = CURRENT_VERSION.load(Ordering::Relaxed); + OSVersion::from_u32(if version == 0 { + // TODO: Consider using `std::panic::abort_unwind` here for code-size? + let version = lookup_version().get(); + CURRENT_VERSION.store(version, Ordering::Relaxed); + version + } else { + version + }) +} + +#[cold] +fn lookup_version() -> NonZeroU32 { + // Since macOS 10.15, libSystem has provided the undocumented + // `_availability_version_check` via `libxpc` for doing this version + // lookup, though it's usage may be a bit dangerous, see: + // - https://reviews.llvm.org/D150397 + // - https://github.com/llvm/llvm-project/issues/64227 + // + // So instead, we use the safer approach of reading from `sysctl`, and + // if that fails, we fall back to the property list (this is what + // `_availability_version_check` does internally). + let version = version_from_sysctl().unwrap_or_else(version_from_plist); + // Use `NonZeroU32` to try to make it clearer to the optimizer that this + // will never return 0. + NonZeroU32::new(version.to_u32()).expect("version cannot be 0.0.0") +} + +/// Read the version from `kern.osproductversion` or `kern.iossupportversion`. +fn version_from_sysctl() -> Option { + // This won't work in the simulator, `kern.osproductversion` will return + // the host macOS version. + if cfg!(target_simulator) { + return None; + } + + // SAFETY: Same signature as in `libc` + extern "C" { + fn sysctlbyname( + name: *const c_char, + oldp: *mut c_void, + oldlenp: *mut usize, + newp: *mut c_void, + newlen: usize, + ) -> c_uint; + } + + let name = if cfg!(target_abi_macabi) { + b"kern.iossupportversion\0".as_ptr().cast() + } else { + // Introduced in macOS 10.13.4. + b"kern.osproductversion\0".as_ptr().cast() + }; + + let mut buf: [u8; 32] = [0; 32]; + let mut size = buf.len(); + let ret = unsafe { sysctlbyname(name, buf.as_mut_ptr().cast(), &mut size, ptr::null_mut(), 0) }; + if ret != 0 { + // `sysctlbyname` is not available. + return None; + } + + Some(OSVersion::from_bytes(&buf[..(size - 1)])) +} + +/// Look up the current OS version from the `ProductVersion` or +/// `iOSSupportVersion` in `/System/Library/CoreServices/SystemVersion.plist`. +/// This file was introduced in macOS 10.3.0. +/// +/// This is also what is done in `compiler-rt`: +/// +/// +/// NOTE: I don't _think_ we need to do a similar thing as what Zig does to +/// handle the fake 10.16 versions returned when the SDK version of the binary +/// is less than 11.0: +/// +/// +/// My reasoning is that we _want_ to follow Apple's behaviour here, and +/// return 10.16 when compiled with an older SDK; the user should upgrade +/// their tooling. +/// +/// NOTE: `rustc` currently doesn't set the right SDK version when linking +/// with ld64, so this will usually have the wrong behaviour on x86_64. But +/// that's a `rustc` bug, and is tracked in: +/// +/// +/// +/// # Panics +/// +/// Panics if reading or parsing the PList fails (or if the system was out of +/// memory). +/// +/// We deliberately choose to panic, as having this lookup silently return +/// an empty OS version would be impossible for a user to debug. +fn version_from_plist() -> OSVersion { + // Use Foundation's mechanisms for reading the PList. + autoreleasepool(|pool| { + let path: Retained = if cfg!(target_simulator) { + let root = std::env::var_os("IPHONE_SIMULATOR_ROOT") + .expect("environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator"); + let path = PathBuf::from(root).join("System/Library/CoreServices/SystemVersion.plist"); + let path = path.as_os_str().as_bytes(); + + // SAFETY: Allocating a string is valid. + let alloc: Allocated = unsafe { msg_send_id![class!(NSString), alloc] }; + // SAFETY: The bytes are valid, and the length is correct. + unsafe { + let bytes_ptr: *const c_void = path.as_ptr().cast(); + msg_send_id![ + alloc, + initWithBytes: bytes_ptr, + length: path.len(), + // OsStr is a superset of UTF-8 on unix platforms + encoding: UTF8_ENCODING, + ] + } + } else { + let path: *const c_char = b"/System/Library/CoreServices/SystemVersion.plist\0" + .as_ptr() + .cast(); + // SAFETY: The path is NULL terminated. + unsafe { msg_send_id![class!(NSString), stringWithUTF8String: path] } + }; + + // SAFETY: dictionaryWithContentsOfFile: is safe to call. + let data: Option> = + unsafe { msg_send_id![class!(NSDictionary), dictionaryWithContentsOfFile: &*path] }; + + let data = data.expect( + "`/System/Library/CoreServices/SystemVersion.plist` must be readable, and contain a valid PList", + ); + + // Read `ProductVersion`, except when running on Mac Catalyst, then we + // read `iOSSupportVersion` instead. + let lookup_key: *const c_char = if cfg!(target_abi_macabi) { + b"iOSSupportVersion\0".as_ptr().cast() + } else { + b"ProductVersion\0".as_ptr().cast() + }; + // SAFETY: The lookup key is NULL terminated. + let lookup_key: Retained = + unsafe { msg_send_id![class!(NSString), stringWithUTF8String: lookup_key] }; + + let version: Retained = + unsafe { msg_send_id![&data, objectForKey: &*lookup_key] }; + + assert!( + version.isKindOfClass(class!(NSString)), + "`ProductVersion` key in `/System/Library/CoreServices/SystemVersion.plist` must be a string" + ); + + // SAFETY: The given object is an NSString, and the returned string + // slice is not used outside of the current pool. + let version = unsafe { nsstring_to_str(&version, pool) }; + + OSVersion::from_str(version) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::string::String; + use std::process::Command; + + #[test] + fn sysctl_same_as_in_plist() { + if let Some(version) = version_from_sysctl() { + assert_eq!(version, version_from_plist()); + } + } + + #[test] + fn read_version() { + assert!(OSVersion::MIN < current_version(), "version cannot be min"); + assert!(current_version() < OSVersion::MAX, "version cannot be max"); + } + + #[test] + #[cfg_attr( + not(target_os = "macos"), + ignore = "`sw_vers` is only available on macOS" + )] + fn compare_against_sw_vers() { + let expected = Command::new("sw_vers") + .arg("-productVersion") + .output() + .unwrap() + .stdout; + let expected = String::from_utf8(expected).unwrap(); + let expected = OSVersion::from_str(expected.trim()); + + let actual = current_version(); + assert_eq!(expected, actual); + } +} diff --git a/crates/objc2/src/ffi/mod.rs b/crates/objc2/src/ffi/mod.rs index 9295ba045..c04d4da7e 100644 --- a/crates/objc2/src/ffi/mod.rs +++ b/crates/objc2/src/ffi/mod.rs @@ -32,21 +32,6 @@ //! This is used by default, and has the highest support priority (all of //! `objc2` will work with this runtime). //! -//! The supported runtime version (higher versions lets the compiler enable -//! newer optimizations, at the cost of not supporting older operating -//! systems) can be chosen using the standard `X_DEPLOYMENT_TARGET` -//! environment variables: -//! -//! - macOS: `MACOSX_DEPLOYMENT_TARGET`, default `10.12`, `11.0` on Aarch64. -//! - iOS / iPadOS: `IPHONEOS_DEPLOYMENT_TARGET`, default `10.0`. -//! - tvOS: `TVOS_DEPLOYMENT_TARGET`, default `10.0`. -//! - watchOS: `WATCHOS_DEPLOYMENT_TARGET`, default `5.0`. -//! -//! The default (and minimum) versions are the [same as those Rust itself -//! has][rust-apple-spec]. -//! -//! [rust-apple-spec]: https://github.com/rust-lang/rust/blob/1.74.0/compiler/rustc_target/src/spec/apple_base.rs -//! //! //! ### GNUStep's [`libobjc2`](https://github.com/gnustep/libobjc2) //! diff --git a/crates/objc2/src/lib.rs b/crates/objc2/src/lib.rs index 557b23ef4..3a969c112 100644 --- a/crates/objc2/src/lib.rs +++ b/crates/objc2/src/lib.rs @@ -255,7 +255,7 @@ compile_error!("ObjFW is not yet supported"); #[cfg_attr(feature = "unstable-objfw", link(name = "objfw-rt", kind = "dylib"))] extern "C" {} -// Link to Foundation to make NSObject work +// Link to Foundation to make NSObject and OS version lookup work. #[cfg_attr(target_vendor = "apple", link(name = "Foundation", kind = "framework"))] #[cfg_attr( all(feature = "gnustep-1-7", not(feature = "unstable-compiler-rt")), diff --git a/crates/objc2/src/macros/available.rs b/crates/objc2/src/macros/available.rs new file mode 100644 index 000000000..d0573d330 --- /dev/null +++ b/crates/objc2/src/macros/available.rs @@ -0,0 +1,193 @@ +/// Check if APIs from the given operating system versions are available. +/// +/// Apple adds new APIs with new OS releases, and as a developer, you often +/// want to use those to give your users the best behaviour, while still +/// supporting older OS versions that don't have those APIs (instead of +/// crashing e.g. because of an undefined selector). +/// +/// This macro allows you to conditionally execute code depending on if the +/// current OS version is higher than or equal to the version given in the +/// macro. +/// +/// If no version is specified for a certain OS, the API will be assumed to be +/// unavailable there. This default can be changed by adding a trailing `..` +/// to the macro invocation. +/// +/// This is very similar to `@available` in Objective-C and `#available` in +/// Swift, see [Apple's documentation][apple-doc]. Another great introduction +/// to availability can be found in [here][epir-availability]. +/// +/// [apple-doc]: https://developer.apple.com/documentation/xcode/running-code-on-a-specific-version#Require-a-minimum-operating-system-version-for-a-feature +/// [epir-availability]: https://epir.at/2019/10/30/api-availability-and-target-conditionals/ +/// +/// +/// # Operating systems +/// +/// The operating system names this macro accepts, the standard environment +/// variables that you use to raise the deployment target (the minimum +/// supported OS version) and the current default versions are all summarized +/// in the table below. +/// +/// | OS Value | Name | Environment Variable | Default | +/// | ---------- | ----------------------- | ---------------------------- | ------- | +/// | `ios` | iOS/iPadOS/Mac Catalyst | `IPHONEOS_DEPLOYMENT_TARGET` | 10.0 | +/// | `macos` | macOS | `MACOSX_DEPLOYMENT_TARGET` | 10.12 | +/// | `tvos` | tvOS | `TVOS_DEPLOYMENT_TARGET` | 10.0 | +/// | `visionos` | visionOS | `XROS_DEPLOYMENT_TARGET` | 1.0 | +/// | `watchos` | watchOS | `WATCHOS_DEPLOYMENT_TARGET` | 5.0 | +/// +/// The default version is the same as that of `rustc` itself. +/// +/// +/// # Optimizations +/// +/// This macro will statically be set to `true` when the deployment target is +/// high enough. +/// +/// If a runtime check is deemed necessary, the version lookup will be cached. +/// +/// +/// # Alternatives +/// +/// Instead of checking the version at runtime, you could do one of the +/// following instead: +/// +/// 1. Check statically that you're compiling for a version where the API is +/// available, e.g. by checking the `*_DEPLOYMENT_TARGET` variables in a +/// build script or at `const` time. +/// +/// 2. Check at runtime that a class, method or symbol is available, using +/// e.g. [`AnyClass::get`], [`respondsToSelector`] or [weak linking]. +/// +/// [`AnyClass::get`]: crate::runtime::AnyClass::get +/// [`respondsToSelector`]: crate::runtime::NSObjectProtocol::respondsToSelector +/// [weak linking]: https://github.com/rust-lang/rust/issues/29603 +/// +/// +/// # Examples +/// +/// Use the [`effectiveAppearance`] API that was added in macOS 10.14. +/// +/// ``` +/// # #[cfg(available_in_frameworks)] +/// use objc2_app_kit::{NSApplication, NSAppearance, NSAppearanceNameAqua}; +/// use objc2::available; +/// +/// let appearance = if available!(macos = 10.14) { +/// // Dark mode and `effectiveAppearance` was added in macOS 10.14. +/// # #[cfg(available_in_frameworks)] +/// NSApplication::sharedApplication(mtm).effectiveAppearance() +/// } else { +/// // Fall back to `NSAppearanceNameAqua` on macOS 10.13 and below. +/// # #[cfg(available_in_frameworks)] +/// NSAppearance::appearanceNamed(NSAppearanceNameAqua).unwrap() +/// }; +/// ``` +/// +/// Use an API added in Xcode 16.0 SDKs. +/// +/// We use `..` here in case Apple adds a new operating system in the future, +/// then we probably also want the branch to be taken there. +/// +/// ``` +/// use objc2::available; +/// +/// if available!(ios = 18.0, macos = 15.0, tvos = 18.0, visionos = 2.0, watchos = 11.0, ..) { +/// // Use some recent API here. +/// } +/// ``` +/// +/// Set the [`wantsExtendedDynamicRangeContent`] property, which is available +/// since iOS 16.0, macOS 10.11 and visionOS 1.0, but is not available on tvOS +/// and watchOS. +/// +/// ``` +/// use objc2::available; +/// +/// if available!(ios = 16.0, macos = 10.11, visionos = 1.0) { +/// # #[cfg(available_in_frameworks)] +/// layer.setWantsExtendedDynamicRangeContent(true); +/// } +/// ``` +/// +/// [`effectiveAppearance`]: https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc +/// [`wantsExtendedDynamicRangeContent`]: https://developer.apple.com/documentation/quartzcore/cametallayer/1478161-wantsextendeddynamicrangecontent +#[doc(alias = "@available")] // Objective-C +#[doc(alias = "#available")] // Swift +#[macro_export] +macro_rules! available { + ( + // Returns `false` on unspecified platforms. + $( + $os:ident $(= $major:literal $(. $minor:literal $(. $patch:literal)?)?)? + ),* $(,)? + ) => { + $crate::__macro_helpers::is_available({ + // TODO: Use inline const once in MSRV + #[allow(clippy::needless_update)] + const VERSION: $crate::__macro_helpers::AvailableVersion = $crate::__macro_helpers::AvailableVersion { + $( + // Doesn't actually parse versions this way, but is + // helpful to write it like this for documentation. + // + // We use optionality for the version here, to allow + // rust-analyzer to work with partially filled macros. + $os: $($crate::__available_version!($major $(. $minor $(. $patch)?)?))?, + )* + // A version this high will never be lower than the deployment + // target, and hence will always return `false` from + // `is_available`. + .. $crate::__macro_helpers::AvailableVersion::MAX + }; + VERSION + }) + }; + ( + // Returns `true` on unspecified platforms because of the trailing `..`. + $( + $os:ident $(= $major:literal $(. $minor:literal $(. $patch:literal)?)?)?, + )* + .. + ) => { + $crate::__macro_helpers::is_available({ + #[allow(clippy::needless_update)] + const VERSION: $crate::__macro_helpers::AvailableVersion = $crate::__macro_helpers::AvailableVersion { + $( + $os: $($crate::__available_version!($major $(. $minor $(. $patch)?)?))?, + )* + // A version of 0.0.0 will always be lower than the deployment + // target, and hence will always return `true` from + // `is_available`. + // + // We do this when `..` is specified. + .. $crate::__macro_helpers::AvailableVersion::MIN + }; + VERSION + }) + }; +} + +/// Both `tt` and `literal` matches either `$major` as an integer, or +/// `$major.$minor` as a float. +/// +/// As such, we cannot just take `$major:tt . $minor:tt . $patch:tt` and +/// convert that to `OSVersion` directly, we must convert it to a string +/// first, and then parse that. +/// +/// We also _have_ to do string parsing, floating point parsing wouldn't be +/// enough (because e.g. `10.10` would result in the float `10.1` and parse +/// wrongly). +/// +/// Note that we intentionally `stringify!` before passing to `concat!`, as +/// that seems to properly preserve all zeros in the literal. +#[doc(hidden)] +#[macro_export] +macro_rules! __available_version { + // Just in case rustc's parsing changes in the future, let's handle this + // generically, instead of trying to split each part into separate `tt`. + ($($version_part_or_period:tt)*) => { + $crate::__macro_helpers::OSVersion::from_str($crate::__macro_helpers::concat!($( + $crate::__macro_helpers::stringify!($version_part_or_period), + )*)) + }; +} diff --git a/crates/objc2/src/macros/mod.rs b/crates/objc2/src/macros/mod.rs index e0225d5ed..a871ccffe 100644 --- a/crates/objc2/src/macros/mod.rs +++ b/crates/objc2/src/macros/mod.rs @@ -2,6 +2,7 @@ mod __attribute_helpers; mod __method_msg_send; mod __msg_send_parse; mod __rewrite_self_param; +mod available; mod declare_class; mod extern_category; mod extern_class; diff --git a/crates/objc2/src/runtime/nsobject.rs b/crates/objc2/src/runtime/nsobject.rs index 4f028ba3b..3de277be0 100644 --- a/crates/objc2/src/runtime/nsobject.rs +++ b/crates/objc2/src/runtime/nsobject.rs @@ -163,34 +163,12 @@ pub unsafe trait NSObjectProtocol { /// /// See [Apple's documentation][apple-doc] for more details. /// - /// [apple-doc]: https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418583-respondstoselector?language=objc - /// - /// - /// # Example - /// - /// Check whether `NSApplication` has the [`effectiveAppearance`] method - /// before calling it, to support systems older than macOS 10.14 where the - /// method was added. + /// If using this for availability checking, you might want to consider + /// using the [`available!`] macro instead, as it is often more + /// performant than this runtime check. /// - /// ``` - /// # #[cfg(available_in_frameworks)] - /// use objc2_app_kit::{NSApplication, NSAppearance, NSAppearanceNameAqua}; - /// use objc2::runtime::NSObjectProtocol; - /// use objc2::sel; - /// - /// # let obj = objc2::runtime::NSObject::new(); - /// # assert!(!obj.respondsToSelector(sel!(effectiveAppearance))); - /// # - /// # #[cfg(available_in_frameworks)] { - /// let appearance = if obj.respondsToSelector(sel!(effectiveAppearance)) { - /// NSApplication::sharedApplication(mtm).effectiveAppearance() - /// } else { - /// unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua).unwrap() } - /// }; - /// # } - /// ``` - /// - /// [`effectiveAppearance`]: https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc + /// [apple-doc]: https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418583-respondstoselector?language=objc + /// [`available!`]: crate::available #[doc(alias = "respondsToSelector:")] fn respondsToSelector(&self, aSelector: Sel) -> bool where diff --git a/crates/test-assembly/crates/test_available/Cargo.toml b/crates/test-assembly/crates/test_available/Cargo.toml new file mode 100644 index 000000000..7cdc8786f --- /dev/null +++ b/crates/test-assembly/crates/test_available/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "test_available" +version = "0.1.0" +edition.workspace = true +publish = false + +[lib] +path = "lib.rs" + +[dependencies] +objc2 = { path = "../../../objc2" } + +[features] +# Runtime +gnustep-1-7 = ["objc2/gnustep-1-7"] +gnustep-1-8 = ["gnustep-1-7", "objc2/gnustep-1-8"] +gnustep-1-9 = ["gnustep-1-8", "objc2/gnustep-1-9"] +gnustep-2-0 = ["gnustep-1-9", "objc2/gnustep-2-0"] +gnustep-2-1 = ["gnustep-2-0", "objc2/gnustep-2-1"] + +# Hack +assembly-features = [] + +[package.metadata.release] +release = false diff --git a/crates/test-assembly/crates/test_available/expected/apple-aarch64.s b/crates/test-assembly/crates/test_available/expected/apple-aarch64.s new file mode 100644 index 000000000..7491d52cb --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-aarch64.s @@ -0,0 +1,87 @@ + .section __TEXT,__text,regular,pure_instructions + .globl _always + .p2align 2 +_always: + mov w0, #1 + ret + + .globl _never + .p2align 2 +_never: + mov w0, #0 + ret + + .globl _low + .p2align 2 +_low: + mov w0, #1 + ret + + .globl _high + .p2align 2 +_high: + stp x20, x19, [sp, #-32]! + stp x29, x30, [sp, #16] + add x29, sp, #16 +Lloh0: + adrp x19, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGE +Lloh1: + ldr x19, [x19, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGEOFF] + ldr w0, [x19] + cbz w0, LBB3_2 +LBB3_1: + lsr w8, w0, #16 + cmp w8, #14 + cset w0, hi + ldp x29, x30, [sp, #16] + ldp x20, x19, [sp], #32 + ret +LBB3_2: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + str w0, [x19] + b LBB3_1 + .loh AdrpLdrGot Lloh0, Lloh1 + + .globl _only_ios + .p2align 2 +_only_ios: + mov w0, #0 + ret + + .globl _two_checks + .p2align 2 +_two_checks: + stp x20, x19, [sp, #-32]! + stp x29, x30, [sp, #16] + add x29, sp, #16 +Lloh2: + adrp x20, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGE +Lloh3: + ldr x20, [x20, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGEOFF] + ldr w19, [x20] + cbz w19, LBB5_3 + ldr w0, [x20] + cbz w0, LBB5_4 +LBB5_2: + lsr w8, w19, #17 + cmp w8, #6 + mov w8, #917503 + add w8, w8, #16, lsl #12 + ccmp w0, w8, #0, hi + cset w0, hi + ldp x29, x30, [sp, #16] + ldp x20, x19, [sp], #32 + ret +LBB5_3: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov x19, x0 + str w0, [x20] + ldr w0, [x20] + cbnz w0, LBB5_2 +LBB5_4: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + str w0, [x20] + b LBB5_2 + .loh AdrpLdrGot Lloh2, Lloh3 + +.subsections_via_symbols diff --git a/crates/test-assembly/crates/test_available/expected/apple-armv7s.s b/crates/test-assembly/crates/test_available/expected/apple-armv7s.s new file mode 100644 index 000000000..12bdebc57 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-armv7s.s @@ -0,0 +1,102 @@ + .section __TEXT,__text,regular,pure_instructions + .syntax unified + .globl _always + .p2align 2 + .code 32 +_always: + mov r0, #1 + bx lr + + .globl _never + .p2align 2 + .code 32 +_never: + mov r0, #0 + bx lr + + .globl _low + .p2align 2 + .code 32 +_low: + mov r0, #1 + bx lr + + .globl _high + .p2align 2 + .code 32 +_high: + push {r4, r7, lr} + add r7, sp, #4 + movw r4, :lower16:(LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC3_0+8)) + movt r4, :upper16:(LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC3_0+8)) +LPC3_0: + ldr r4, [pc, r4] + ldr r0, [r4] + cmp r0, #0 + beq LBB3_2 +LBB3_1: + lsr r1, r0, #17 + mov r0, #0 + cmp r1, #8 + movwhi r0, #1 + pop {r4, r7, pc} +LBB3_2: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + str r0, [r4] + b LBB3_1 + + .globl _only_ios + .p2align 2 + .code 32 +_only_ios: + mov r0, #1 + bx lr + + .globl _two_checks + .p2align 2 + .code 32 +_two_checks: + push {r4, r5, r7, lr} + add r7, sp, #8 + movw r5, :lower16:(LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC5_0+8)) + movt r5, :upper16:(LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC5_0+8)) +LPC5_0: + ldr r5, [pc, r5] + ldr r4, [r5] + cmp r4, #0 + beq LBB5_3 + ldr r0, [r5] + cmp r0, #0 + beq LBB5_4 +LBB5_2: + lsr r1, r4, #16 + mov r2, #0 + cmp r1, #16 + mov r1, #0 + movwhi r1, #1 + movw r3, #65535 + movt r3, #16 + orr r3, r3, #65536 + cmp r0, r3 + movwhi r2, #1 + and r0, r1, r2 + pop {r4, r5, r7, pc} +LBB5_3: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov r4, r0 + str r0, [r5] + ldr r0, [r5] + cmp r0, #0 + bne LBB5_2 +LBB5_4: + bl SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + str r0, [r5] + b LBB5_2 + + .section __DATA,__nl_symbol_ptr,non_lazy_symbol_pointers + .p2align 2, 0x0 +LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr: + .indirect_symbol SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0) + .long 0 + +.subsections_via_symbols diff --git a/crates/test-assembly/crates/test_available/expected/apple-old-x86.s b/crates/test-assembly/crates/test_available/expected/apple-old-x86.s new file mode 100644 index 000000000..69fb84aaa --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-old-x86.s @@ -0,0 +1,109 @@ + .section __TEXT,__text,regular,pure_instructions + .intel_syntax noprefix + .globl _always + .p2align 4, 0x90 +_always: + push ebp + mov ebp, esp + mov al, 1 + pop ebp + ret + + .globl _never + .p2align 4, 0x90 +_never: + push ebp + mov ebp, esp + xor eax, eax + pop ebp + ret + + .globl _low + .p2align 4, 0x90 +_low: + push ebp + mov ebp, esp + mov al, 1 + pop ebp + ret + + .globl _high + .p2align 4, 0x90 +_high: + push ebp + mov ebp, esp + push esi + push eax + call L3$pb +L3$pb: + pop eax + mov esi, dword ptr [eax + LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-L3$pb] + mov eax, dword ptr [esi] + test eax, eax + je LBB3_1 +LBB3_2: + cmp eax, 983040 + setae al + add esp, 4 + pop esi + pop ebp + ret +LBB3_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [esi], eax + jmp LBB3_2 + + .globl _only_ios + .p2align 4, 0x90 +_only_ios: + push ebp + mov ebp, esp + xor eax, eax + pop ebp + ret + + .globl _two_checks + .p2align 4, 0x90 +_two_checks: + push ebp + mov ebp, esp + push edi + push esi + call L5$pb +L5$pb: + pop eax + mov edi, dword ptr [eax + LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-L5$pb] + mov esi, dword ptr [edi] + test esi, esi + je LBB5_1 + mov eax, dword ptr [edi] + test eax, eax + je LBB5_3 +LBB5_4: + cmp esi, 917504 + setae cl + cmp eax, 983040 + setae al + and al, cl + pop esi + pop edi + pop ebp + ret +LBB5_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov esi, eax + mov dword ptr [edi], eax + mov eax, dword ptr [edi] + test eax, eax + jne LBB5_4 +LBB5_3: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [edi], eax + jmp LBB5_4 + + .section __IMPORT,__pointers,non_lazy_symbol_pointers +LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr: + .indirect_symbol SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0) + .long 0 + +.subsections_via_symbols diff --git a/crates/test-assembly/crates/test_available/expected/apple-x86.s b/crates/test-assembly/crates/test_available/expected/apple-x86.s new file mode 100644 index 000000000..d275f2378 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-x86.s @@ -0,0 +1,109 @@ + .section __TEXT,__text,regular,pure_instructions + .intel_syntax noprefix + .globl _always + .p2align 4, 0x90 +_always: + push ebp + mov ebp, esp + mov al, 1 + pop ebp + ret + + .globl _never + .p2align 4, 0x90 +_never: + push ebp + mov ebp, esp + xor eax, eax + pop ebp + ret + + .globl _low + .p2align 4, 0x90 +_low: + push ebp + mov ebp, esp + mov al, 1 + pop ebp + ret + + .globl _high + .p2align 4, 0x90 +_high: + push ebp + mov ebp, esp + push esi + push eax + call L3$pb +L3$pb: + pop eax + mov esi, dword ptr [eax + LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-L3$pb] + mov eax, dword ptr [esi] + test eax, eax + je LBB3_1 +LBB3_2: + cmp eax, 1179648 + setae al + add esp, 4 + pop esi + pop ebp + ret +LBB3_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [esi], eax + jmp LBB3_2 + + .globl _only_ios + .p2align 4, 0x90 +_only_ios: + push ebp + mov ebp, esp + mov al, 1 + pop ebp + ret + + .globl _two_checks + .p2align 4, 0x90 +_two_checks: + push ebp + mov ebp, esp + push edi + push esi + call L5$pb +L5$pb: + pop eax + mov edi, dword ptr [eax + LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-L5$pb] + mov esi, dword ptr [edi] + test esi, esi + je LBB5_1 + mov eax, dword ptr [edi] + test eax, eax + je LBB5_3 +LBB5_4: + cmp esi, 1114112 + setae cl + cmp eax, 1179648 + setae al + and al, cl + pop esi + pop edi + pop ebp + ret +LBB5_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov esi, eax + mov dword ptr [edi], eax + mov eax, dword ptr [edi] + test eax, eax + jne LBB5_4 +LBB5_3: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [edi], eax + jmp LBB5_4 + + .section __IMPORT,__pointers,non_lazy_symbol_pointers +LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr: + .indirect_symbol SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0) + .long 0 + +.subsections_via_symbols diff --git a/crates/test-assembly/crates/test_available/expected/apple-x86_64.s b/crates/test-assembly/crates/test_available/expected/apple-x86_64.s new file mode 100644 index 000000000..36c4ee1fe --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-x86_64.s @@ -0,0 +1,98 @@ + .section __TEXT,__text,regular,pure_instructions + .intel_syntax noprefix + .globl _always + .p2align 4, 0x90 +_always: + push rbp + mov rbp, rsp + mov al, 1 + pop rbp + ret + + .globl _never + .p2align 4, 0x90 +_never: + push rbp + mov rbp, rsp + xor eax, eax + pop rbp + ret + + .globl _low + .p2align 4, 0x90 +_low: + push rbp + mov rbp, rsp + mov al, 1 + pop rbp + ret + + .globl _high + .p2align 4, 0x90 +_high: + push rbp + mov rbp, rsp + push rbx + push rax + mov rbx, qword ptr [rip + SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPCREL] + mov eax, dword ptr [rbx] + test eax, eax + je LBB3_1 +LBB3_2: + cmp eax, 983040 + setae al + add rsp, 8 + pop rbx + pop rbp + ret +LBB3_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [rbx], eax + jmp LBB3_2 + + .globl _only_ios + .p2align 4, 0x90 +_only_ios: + push rbp + mov rbp, rsp + xor eax, eax + pop rbp + ret + + .globl _two_checks + .p2align 4, 0x90 +_two_checks: + push rbp + mov rbp, rsp + push r14 + push rbx + mov r14, qword ptr [rip + SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPCREL] + mov ebx, dword ptr [r14] + test ebx, ebx + je LBB5_1 + mov eax, dword ptr [r14] + test eax, eax + je LBB5_3 +LBB5_4: + cmp ebx, 917504 + setae cl + cmp eax, 983040 + setae al + and al, cl + pop rbx + pop r14 + pop rbp + ret +LBB5_1: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov ebx, eax + mov dword ptr [r14], eax + mov eax, dword ptr [r14] + test eax, eax + jne LBB5_4 +LBB5_3: + call SYM(objc2::__macro_helpers::os_version::apple::lookup_version::GENERATED_ID, 0) + mov dword ptr [r14], eax + jmp LBB5_4 + +.subsections_via_symbols diff --git a/crates/test-assembly/crates/test_available/expected/gnustep-x86.s b/crates/test-assembly/crates/test_available/expected/gnustep-x86.s new file mode 100644 index 000000000..8043d973f --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/gnustep-x86.s @@ -0,0 +1,63 @@ + .text + .intel_syntax noprefix + .section .text.always,"ax",@progbits + .globl always + .p2align 4, 0x90 + .type always,@function +always: + mov al, 1 + ret +.Lfunc_end0: + .size always, .Lfunc_end0-always + + .section .text.never,"ax",@progbits + .globl never + .p2align 4, 0x90 + .type never,@function +never: + xor eax, eax + ret +.Lfunc_end1: + .size never, .Lfunc_end1-never + + .section .text.low,"ax",@progbits + .globl low + .p2align 4, 0x90 + .type low,@function +low: + mov al, 1 + ret +.Lfunc_end2: + .size low, .Lfunc_end2-low + + .section .text.high,"ax",@progbits + .globl high + .p2align 4, 0x90 + .type high,@function +high: + mov al, 1 + ret +.Lfunc_end3: + .size high, .Lfunc_end3-high + + .section .text.only_ios,"ax",@progbits + .globl only_ios + .p2align 4, 0x90 + .type only_ios,@function +only_ios: + xor eax, eax + ret +.Lfunc_end4: + .size only_ios, .Lfunc_end4-only_ios + + .section .text.two_checks,"ax",@progbits + .globl two_checks + .p2align 4, 0x90 + .type two_checks,@function +two_checks: + mov al, 1 + ret +.Lfunc_end5: + .size two_checks, .Lfunc_end5-two_checks + + .section ".note.GNU-stack","",@progbits diff --git a/crates/test-assembly/crates/test_available/expected/gnustep-x86_64.s b/crates/test-assembly/crates/test_available/expected/gnustep-x86_64.s new file mode 100644 index 000000000..8043d973f --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/gnustep-x86_64.s @@ -0,0 +1,63 @@ + .text + .intel_syntax noprefix + .section .text.always,"ax",@progbits + .globl always + .p2align 4, 0x90 + .type always,@function +always: + mov al, 1 + ret +.Lfunc_end0: + .size always, .Lfunc_end0-always + + .section .text.never,"ax",@progbits + .globl never + .p2align 4, 0x90 + .type never,@function +never: + xor eax, eax + ret +.Lfunc_end1: + .size never, .Lfunc_end1-never + + .section .text.low,"ax",@progbits + .globl low + .p2align 4, 0x90 + .type low,@function +low: + mov al, 1 + ret +.Lfunc_end2: + .size low, .Lfunc_end2-low + + .section .text.high,"ax",@progbits + .globl high + .p2align 4, 0x90 + .type high,@function +high: + mov al, 1 + ret +.Lfunc_end3: + .size high, .Lfunc_end3-high + + .section .text.only_ios,"ax",@progbits + .globl only_ios + .p2align 4, 0x90 + .type only_ios,@function +only_ios: + xor eax, eax + ret +.Lfunc_end4: + .size only_ios, .Lfunc_end4-only_ios + + .section .text.two_checks,"ax",@progbits + .globl two_checks + .p2align 4, 0x90 + .type two_checks,@function +two_checks: + mov al, 1 + ret +.Lfunc_end5: + .size two_checks, .Lfunc_end5-two_checks + + .section ".note.GNU-stack","",@progbits diff --git a/crates/test-assembly/crates/test_available/lib.rs b/crates/test-assembly/crates/test_available/lib.rs new file mode 100644 index 000000000..1dc1e7420 --- /dev/null +++ b/crates/test-assembly/crates/test_available/lib.rs @@ -0,0 +1,69 @@ +//! Test that the `available!` macro is optimized as expected. +use objc2::available; + +#[no_mangle] +fn always() -> bool { + // Can elide the version check here + available!(..) +} + +#[no_mangle] +fn never() -> bool { + // Can elide the version check here + available!() +} + +#[no_mangle] +fn low() -> bool { + // Can elide the version check here + available!( + macos = 10.7, + ios = 5.0, + tvos = 5.0, + watchos = 3.0, + visionos = 1.0, + .. + ) +} + +#[no_mangle] +fn high() -> bool { + // Has to insert a runtime check here + available!( + macos = 15.0, + ios = 18.0, + tvos = 18.0, + watchos = 11.0, + visionos = 2.0, + .. + ) +} + +#[no_mangle] +fn only_ios() -> bool { + // Can elide the version check here on macOS + available!(ios = 5.0) +} + +#[no_mangle] +fn two_checks() -> bool { + // Ideally only has to insert one runtime check here, but currently does two. + + let in_14 = available!( + macos = 14.0, + ios = 17.0, + tvos = 17.0, + watchos = 10.0, + visionos = 1.0, + .. + ); + let in_15 = available!( + macos = 15.0, + ios = 18.0, + tvos = 18.0, + watchos = 11.0, + visionos = 2.0, + .. + ); + in_14 && in_15 +} diff --git a/crates/test-ui/ui/available_invalid.rs b/crates/test-ui/ui/available_invalid.rs new file mode 100644 index 000000000..96ad120ed --- /dev/null +++ b/crates/test-ui/ui/available_invalid.rs @@ -0,0 +1,13 @@ +//! Various invalid usage of the `available!` macro. +use objc2::available; + +fn main() { + // Space between version + available!(macos = 1 1); + + // Various invalid syntax + available!(macos = ABCD); + available!(macos = ); + available!(macos: 1.2); + available!(macos); +} diff --git a/crates/test-ui/ui/available_invalid.stderr b/crates/test-ui/ui/available_invalid.stderr new file mode 100644 index 000000000..9e9c08026 --- /dev/null +++ b/crates/test-ui/ui/available_invalid.stderr @@ -0,0 +1,50 @@ +error: no rules expected the token `1` + --> ui/available_invalid.rs + | + | available!(macos = 1 1); + | ^ no rules expected this token in macro call + | + = note: while trying to match sequence start + +error: no rules expected the token `ABCD` + --> ui/available_invalid.rs + | + | available!(macos = ABCD); + | ^^^^ no rules expected this token in macro call + | +note: while trying to match meta-variable `$major:literal` + --> $WORKSPACE/crates/objc2/src/macros/available.rs + | + | $os:ident $(= $major:literal $(. $minor:literal $(. $patch:literal)?)?)? + | ^^^^^^^^^^^^^^ + +error: unexpected end of macro invocation + --> ui/available_invalid.rs + | + | available!(macos = ); + | ^ missing tokens in macro arguments + | +note: while trying to match meta-variable `$major:literal` + --> $WORKSPACE/crates/objc2/src/macros/available.rs + | + | $os:ident $(= $major:literal $(. $minor:literal $(. $patch:literal)?)?)? + | ^^^^^^^^^^^^^^ + +error: no rules expected the token `:` + --> ui/available_invalid.rs + | + | available!(macos: 1.2); + | ^ no rules expected this token in macro call + | + = note: while trying to match sequence start + +error: expected expression, found `,` + --> ui/available_invalid.rs + | + | available!(macos); + | ^^^^^^^^^^^^^^^^^ + | | + | expected expression + | while parsing this struct + | + = note: this error originates in the macro `available` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/test-ui/ui/available_same_os.rs b/crates/test-ui/ui/available_same_os.rs new file mode 100644 index 000000000..301dbcab9 --- /dev/null +++ b/crates/test-ui/ui/available_same_os.rs @@ -0,0 +1,6 @@ +//! Same OS name repeated in the `available!` macro. +use objc2::available; + +fn main() { + available!(macos = 1.2, macos = 1.2); +} diff --git a/crates/test-ui/ui/available_same_os.stderr b/crates/test-ui/ui/available_same_os.stderr new file mode 100644 index 000000000..b1cae28de --- /dev/null +++ b/crates/test-ui/ui/available_same_os.stderr @@ -0,0 +1,8 @@ +error[E0062]: field `macos` specified more than once + --> ui/available_same_os.rs + | + | available!(macos = 1.2, macos = 1.2); + | ------------------------^^^^^------- + | | | + | | used more than once + | first use of `macos` diff --git a/crates/test-ui/ui/available_unknown_os.rs b/crates/test-ui/ui/available_unknown_os.rs new file mode 100644 index 000000000..4c89111a7 --- /dev/null +++ b/crates/test-ui/ui/available_unknown_os.rs @@ -0,0 +1,6 @@ +//! Unknown OS name in `available!` macro. +use objc2::available; + +fn main() { + available!(unknown = 1.2); +} diff --git a/crates/test-ui/ui/available_unknown_os.stderr b/crates/test-ui/ui/available_unknown_os.stderr new file mode 100644 index 000000000..fae589d05 --- /dev/null +++ b/crates/test-ui/ui/available_unknown_os.stderr @@ -0,0 +1,7 @@ +error[E0560]: struct `AvailableVersion` has no field named `unknown` + --> ui/available_unknown_os.rs + | + | available!(unknown = 1.2); + | ^^^^^^^ `AvailableVersion` does not have this field + | + = note: all struct fields are already assigned