diff --git a/Cargo.lock b/Cargo.lock index 8e1484776..af0180e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,6 +1378,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..054d5d313 --- /dev/null +++ b/crates/objc2/src/__macro_helpers/os_version.rs @@ -0,0 +1,383 @@ +//! 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 { + /// 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 + } +} + +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. +/// +/// We use `#[cfg]`s explicitly to allow the user to omit an annotation for +/// a specific platform if they are never gonna need it, while still failing +/// with a compile error if the code ends up being compiled for that platform. +#[derive(Clone, Copy, Debug)] +pub struct AvailableVersion { + #[cfg(target_os = "macos")] + pub macos: OSVersion, + #[cfg(target_os = "ios")] + pub ios: OSVersion, + #[cfg(target_os = "tvos")] + pub tvos: OSVersion, + #[cfg(target_os = "watchos")] + pub watchos: OSVersion, + #[cfg(target_os = "visionos")] + pub visionos: OSVersion, +} + +impl AvailableVersion { + pub const DOTDOT_FALLBACK: Self = { + // A version of 0.0.0 will always be lower than the deployment target, + // and hence will always return `true` from `is_available`. + let _version = OSVersion { + major: 0, + minor: 0, + patch: 0, + }; + Self { + #[cfg(target_os = "macos")] + macos: _version, + #[cfg(target_os = "ios")] + ios: _version, + #[cfg(target_os = "tvos")] + tvos: _version, + #[cfg(target_os = "watchos")] + watchos: _version, + #[cfg(target_os = "visionos")] + visionos: _version, + } + }; +} + +#[inline] +#[cfg(not(target_vendor = "apple"))] +pub fn is_available(_version: AvailableVersion) -> bool { + // Assume that things are always available on GNUStep etc. + true +} + +#[inline] +#[cfg(target_vendor = "apple")] +pub fn is_available(version: AvailableVersion) -> bool { + #[cfg(target_os = "macos")] + let version = version.macos; + #[cfg(target_os = "ios")] + let version = version.ios; + #[cfg(target_os = "tvos")] + let version = version.tvos; + #[cfg(target_os = "watchos")] + let version = version.watchos; + #[cfg(target_os = "visionos")] + let version = version.visionos; + + // 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(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!(..)); + + // 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 + )); + } +} 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..3a26d8545 --- /dev/null +++ b/crates/objc2/src/__macro_helpers/os_version/apple.rs @@ -0,0 +1,293 @@ +use core::ffi::{c_char, c_uint, c_void}; +use core::ptr; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; +use std::sync::OnceLock; + +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. + // + // TODO: Maybe just use atomics, a `Once` seems like overkill, it doesn't + // matter if two threads end up racing to read the version? + static CURRENT_VERSION: OnceLock = OnceLock::new(); + + *CURRENT_VERSION.get_or_init(|| { + // 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). + version_from_sysctl().unwrap_or_else(version_from_plist) + }) +} + +/// 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!( + current_version() + > OSVersion { + major: 0, + minor: 0, + patch: 0, + }, + "version cannot be 0.0.0" + ); + } + + #[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..e5a7090fd 100644 --- a/crates/objc2/src/ffi/mod.rs +++ b/crates/objc2/src/ffi/mod.rs @@ -37,15 +37,16 @@ //! systems) can be chosen using the standard `X_DEPLOYMENT_TARGET` //! environment variables: //! -//! - macOS: `MACOSX_DEPLOYMENT_TARGET`, default `10.12`, `11.0` on Aarch64. +//! - macOS: `MACOSX_DEPLOYMENT_TARGET`, default `10.12`. //! - iOS / iPadOS: `IPHONEOS_DEPLOYMENT_TARGET`, default `10.0`. //! - tvOS: `TVOS_DEPLOYMENT_TARGET`, default `10.0`. //! - watchOS: `WATCHOS_DEPLOYMENT_TARGET`, default `5.0`. +//! - visionOS: `XROS_DEPLOYMENT_TARGET`, default `1.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 +//! [rust-apple-spec]: https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L207-L231 //! //! //! ### 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..fc1769166 --- /dev/null +++ b/crates/objc2/src/macros/available.rs @@ -0,0 +1,192 @@ +/// TODO. +/// +/// If no version is specified for a certain OS / platform, the API will be assumed to be available. +/// +/// Design choice: Explicitly do not default here, to make it a compile error +/// when not specified for the OS we're compiling for. +/// +/// TODO: Or should we make that a compile error when compiling for that OS? +#[doc(alias = "@available")] // Objective-C +#[doc(alias = "#available")] // Swift +#[macro_export] +macro_rules! available { + ( + $( + $os:ident = $major:literal $(. $minor:literal $(. $patch:literal)?)? + ),+ + $(,)? + ) => { + $crate::__available_fields!( + () + $( + ($os, $os, $crate::__available_version!($major $(. $minor $(. $patch)?)?)) + )* + ) + }; + ( + // Doesn't actually parse versions this way (see __available_version), + // but is helpful to write it like this for documentation. + $( + $os:ident = $major:literal $(. $minor:literal $(. $patch:literal)?)?, + )* + // Allow missing versions when `..` is specified, by spreading + // `AvailableVersion::DOTDOT_FALLBACK` in that case. + .. + ) => { + $crate::__available_fields!( + () + $( + ($os, $os, $crate::__available_version!($major $(. $minor $(. $patch)?)?)) + )* + .. + ) + }; +} + +/// tt-muncher, we need to handle each OS name manually, since `target_os` +/// only supports fixed literals (we can't do `target_os = stringify!($os)`). +#[doc(hidden)] +#[macro_export] +macro_rules! __available_fields { + // Base case + ( + ($($output:tt)*) + ) => { + $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 { + $($output)* + }; + VERSION + }) + }; + // macOS + ( + ($($output:tt)*) + (macos, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + #[cfg(target_os = "macos")] + $os: $version, + ) + $($rest)* + ) + }; + // iOS + ( + ($($output:tt)*) + (ios, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + #[cfg(target_os = "ios")] + $os: $version, + ) + $($rest)* + ) + }; + // tvOS + ( + ($($output:tt)*) + (tvos, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + #[cfg(target_os = "tvos")] + $os: $version, + ) + $($rest)* + ) + }; + // watchOS + ( + ($($output:tt)*) + (watchos, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + #[cfg(target_os = "watchos")] + $os: $version, + ) + $($rest)* + ) + }; + // visionOS + ( + ($($output:tt)*) + (visionos, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + #[cfg(target_os = "visionos")] + $os: $version, + ) + $($rest)* + ) + }; + // Unknown OS + ( + ($($output:tt)*) + ($unknown_os:ident, $os:ident, $version:expr) + $($rest:tt)* + ) => { + $crate::__available_fields!( + ( + $($output)* + // It's fine to output the version for unknown OS-es here, + // since `AvailableVersion` is going to catch the mistake. + $os: $version, + ) + $($rest)* + ) + }; + // .. marker. + ( + ($($output:tt)*) + .. + ) => { + $crate::__available_fields!( + ( + $($output)* + .. $crate::__macro_helpers::AvailableVersion::DOTDOT_FALLBACK + ) + ) + }; +} + +/// 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/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..21b56a440 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-aarch64.s @@ -0,0 +1,156 @@ + .section __TEXT,__text,regular,pure_instructions + .p2align 2 +SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0): +Lloh0: + adrp x0, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGE +Lloh1: + ldr x0, [x0, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGEOFF] + ldapr x8, [x0] + cmp x8, #3 + b.ne LBB0_2 + ret +LBB0_2: + sub sp, sp, #48 + stp x29, x30, [sp, #32] + add x29, sp, #32 + add x8, x0, #8 + sub x9, x29, #1 + stp x8, x9, [sp] + mov x8, sp + str x8, [sp, #16] +Lloh2: + adrp x3, l_anon.[ID].0@PAGE +Lloh3: + add x3, x3, l_anon.[ID].0@PAGEOFF +Lloh4: + adrp x4, l_anon.[ID].2@PAGE +Lloh5: + add x4, x4, l_anon.[ID].2@PAGEOFF + add x2, sp, #16 + mov w1, #1 + bl SYM(std::sys::sync::once::queue::Once::call::GENERATED_ID, 0) + ldp x29, x30, [sp, #32] + add sp, sp, #48 + ret + .loh AdrpLdrGot Lloh0, Lloh1 + .loh AdrpAdd Lloh4, Lloh5 + .loh AdrpAdd Lloh2, Lloh3 + + .p2align 2 +SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0): + stp x20, x19, [sp, #-32]! + stp x29, x30, [sp, #16] + add x29, sp, #16 + ldr x8, [x0] + ldr x19, [x8] + str xzr, [x8] + cbz x19, LBB1_5 + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + tst w0, #0xffff + b.eq LBB1_3 + lsr x0, x0, #16 + b LBB1_4 +LBB1_3: + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB1_4: + str w0, [x19] + ldp x29, x30, [sp, #16] + ldp x20, x19, [sp], #32 + ret +LBB1_5: +Lloh6: + adrp x0, l_anon.[ID].3@PAGE +Lloh7: + add x0, x0, l_anon.[ID].3@PAGEOFF + bl SYM(core::option::unwrap_failed::GENERATED_ID, 0) + .loh AdrpAdd Lloh6, Lloh7 + + .p2align 2 +SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0): + stp x20, x19, [sp, #-32]! + stp x29, x30, [sp, #16] + add x29, sp, #16 + ldr x8, [x0] + ldr x19, [x8] + str xzr, [x8] + cbz x19, LBB2_5 + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + tst w0, #0xffff + b.eq LBB2_3 + lsr x0, x0, #16 + b LBB2_4 +LBB2_3: + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB2_4: + str w0, [x19] + ldp x29, x30, [sp, #16] + ldp x20, x19, [sp], #32 + ret +LBB2_5: +Lloh8: + adrp x0, l_anon.[ID].3@PAGE +Lloh9: + add x0, x0, l_anon.[ID].3@PAGEOFF + bl SYM(core::option::unwrap_failed::GENERATED_ID, 0) + .loh AdrpAdd Lloh8, Lloh9 + + .globl _always + .p2align 2 +_always: + mov w0, #1 + 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 +Lloh10: + adrp x19, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGE +Lloh11: + ldr x19, [x19, SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPAGEOFF] + ldapr x8, [x19] + cmp x8, #3 + b.ne LBB5_2 +LBB5_1: + ldrh w8, [x19, #10] + cmp w8, #14 + cset w0, hi + ldp x29, x30, [sp, #16] + ldp x20, x19, [sp], #32 + ret +LBB5_2: + bl SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0) + b LBB5_1 + .loh AdrpLdrGot Lloh10, Lloh11 + + .section __DATA,__const + .p2align 3, 0x0 +l_anon.[ID].0: + .asciz "\000\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\b\000\000\000\000\000\000" + .quad SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0) + .quad SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0) + + .section __TEXT,__const +l_anon.[ID].1: + .ascii "$RUSTC/library/std/src/sync/once.rs" + + .section __DATA,__const + .p2align 3, 0x0 +l_anon.[ID].2: + .quad l_anon.[ID].1 + .asciz "p\000\000\000\000\000\000\000\331\000\000\000\024\000\000" + + .p2align 3, 0x0 +l_anon.[ID].3: + .quad l_anon.[ID].1 + .asciz "p\000\000\000\000\000\000\000\331\000\000\0001\000\000" + +.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..63875825e --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-armv7s.s @@ -0,0 +1,179 @@ + .section __TEXT,__text,regular,pure_instructions + .syntax unified + .p2align 2 + .code 32 +SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0): + ldr r0, LCPI0_0 +LPC0_0: + ldr r0, [pc, r0] + ldr r1, [r0] + dmb ish + cmp r1, #3 + bxeq lr +LBB0_1: + push {r2, r3, r4, r5, r6, r7, lr} + add r7, sp, #20 + add r1, r0, #4 + str r1, [sp, #4] + sub r1, r7, #1 + str r1, [sp, #8] + add r1, sp, #4 + str r1, [r7, #-8] + ldr r1, LCPI0_1 +LPC0_1: + add r1, pc, r1 + ldr r3, LCPI0_2 +LPC0_2: + add r3, pc, r3 + str r1, [sp] + sub r2, r7, #8 + mov r1, #1 + bl SYM(std::sys::sync::once::queue::Once::call::GENERATED_ID, 0) + mov sp, r7 + pop {r7, lr} + bx lr + .p2align 2 + .data_region +LCPI0_0: + .long LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC0_0+8) +LCPI0_1: + .long l_anon.[ID].2-(LPC0_1+8) +LCPI0_2: + .long l_anon.[ID].0-(LPC0_2+8) + .end_data_region + + .p2align 2 + .code 32 +SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0): + push {r4, r7, lr} + add r7, sp, #4 + sub sp, sp, #8 + ldr r0, [r0] + ldr r4, [r0] + mov r1, #0 + str r1, [r0] + cmp r4, #0 + beq LBB1_4 + add r0, sp, #2 + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + ldrh r0, [sp, #2] + cmp r0, #0 + beq LBB1_3 + ldr r0, [sp, #4] + str r0, [r4] + sub sp, r7, #4 + pop {r4, r7, pc} +LBB1_3: + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) + str r0, [r4] + sub sp, r7, #4 + pop {r4, r7, pc} +LBB1_4: + movw r0, :lower16:(l_anon.[ID].3-(LPC1_0+8)) + movt r0, :upper16:(l_anon.[ID].3-(LPC1_0+8)) +LPC1_0: + add r0, pc, r0 + mov lr, pc + b SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .p2align 2 + .code 32 +SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0): + push {r4, r7, lr} + add r7, sp, #4 + sub sp, sp, #8 + ldr r0, [r0] + ldr r4, [r0] + mov r1, #0 + str r1, [r0] + cmp r4, #0 + beq LBB2_4 + add r0, sp, #2 + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + ldrh r0, [sp, #2] + cmp r0, #0 + beq LBB2_3 + ldr r0, [sp, #4] + str r0, [r4] + sub sp, r7, #4 + pop {r4, r7, pc} +LBB2_3: + bl SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) + str r0, [r4] + sub sp, r7, #4 + pop {r4, r7, pc} +LBB2_4: + movw r0, :lower16:(l_anon.[ID].3-(LPC2_0+8)) + movt r0, :upper16:(l_anon.[ID].3-(LPC2_0+8)) +LPC2_0: + add r0, pc, r0 + mov lr, pc + b SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .globl _always + .p2align 2 + .code 32 +_always: + mov r0, #1 + 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-(LPC5_0+8)) + movt r4, :upper16:(LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-(LPC5_0+8)) +LPC5_0: + ldr r4, [pc, r4] + ldr r0, [r4] + dmb ish + cmp r0, #3 + bne LBB5_2 +LBB5_1: + ldrh r1, [r4, #6] + mov r0, #0 + cmp r1, #17 + movwhi r0, #1 + pop {r4, r7, pc} +LBB5_2: + bl SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0) + b LBB5_1 + + .section __DATA,__const + .p2align 2, 0x0 +l_anon.[ID].0: + .asciz "\000\000\000\000\004\000\000\000\004\000\000" + .long SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0) + .long SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0) + + .section __TEXT,__const +l_anon.[ID].1: + .ascii "$RUSTC/library/std/src/sync/once.rs" + + .section __DATA,__const + .p2align 2, 0x0 +l_anon.[ID].2: + .long l_anon.[ID].1 + .asciz "p\000\000\000\331\000\000\000\024\000\000" + + .p2align 2, 0x0 +l_anon.[ID].3: + .long l_anon.[ID].1 + .asciz "p\000\000\000\331\000\000\0001\000\000" + + .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-x86.s b/crates/test-assembly/crates/test_available/expected/apple-x86.s new file mode 100644 index 000000000..1f306c147 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-x86.s @@ -0,0 +1,182 @@ + .section __TEXT,__text,regular,pure_instructions + .intel_syntax noprefix +SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0): + push ebp + mov ebp, esp + push esi + sub esp, 20 + call L0$pb +L0$pb: + pop ecx + mov eax, dword ptr [ecx + LSYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)$non_lazy_ptr-L0$pb] + mov edx, dword ptr [eax] + cmp edx, 3 + jne LBB0_1 +LBB0_2: + add esp, 20 + pop esi + pop ebp + ret +LBB0_1: + lea edx, [eax + 4] + lea esi, [ebp - 20] + mov dword ptr [esi], edx + lea edx, [ebp - 5] + mov dword ptr [esi + 4], edx + lea edx, [ebp - 12] + mov dword ptr [edx], esi + sub esp, 12 + lea esi, [ecx + l_anon.[ID].2-L0$pb] + lea ecx, [ecx + l_anon.[ID].0-L0$pb] + push esi + push ecx + push edx + push 1 + push eax + call SYM(std::sys::sync::once::queue::Once::call::GENERATED_ID, 0) + add esp, 32 + jmp LBB0_2 + + .p2align 4, 0x90 +SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0): + push ebp + mov ebp, esp + push esi + sub esp, 20 + call L1$pb +L1$pb: + pop eax + mov ecx, dword ptr [ebp + 8] + mov ecx, dword ptr [ecx] + mov esi, dword ptr [ecx] + mov dword ptr [ecx], 0 + test esi, esi + je LBB1_5 + lea eax, [ebp - 10] + mov dword ptr [esp], eax + call SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + sub esp, 4 + test byte ptr [ebp - 10], 1 + je LBB1_2 + mov eax, dword ptr [ebp - 8] + jmp LBB1_4 +LBB1_2: + call SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB1_4: + mov dword ptr [esi], eax + add esp, 20 + pop esi + pop ebp + ret +LBB1_5: + lea eax, [eax + l_anon.[ID].3-L1$pb] + mov dword ptr [esp], eax + call SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .p2align 4, 0x90 +SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0): + push ebp + mov ebp, esp + push esi + sub esp, 20 + call L2$pb +L2$pb: + pop eax + mov ecx, dword ptr [ebp + 8] + mov ecx, dword ptr [ecx] + mov esi, dword ptr [ecx] + mov dword ptr [ecx], 0 + test esi, esi + je LBB2_5 + lea eax, [ebp - 10] + mov dword ptr [esp], eax + call SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + sub esp, 4 + test byte ptr [ebp - 10], 1 + je LBB2_2 + mov eax, dword ptr [ebp - 8] + jmp LBB2_4 +LBB2_2: + call SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB2_4: + mov dword ptr [esi], eax + add esp, 20 + pop esi + pop ebp + ret +LBB2_5: + lea eax, [eax + l_anon.[ID].3-L2$pb] + mov dword ptr [esp], eax + call SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .globl _always + .p2align 4, 0x90 +_always: + push ebp + mov ebp, esp + mov al, 1 + 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 L5$pb +L5$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-L5$pb] + mov eax, dword ptr [esi] + cmp eax, 3 + jne LBB5_1 +LBB5_2: + cmp word ptr [esi + 6], 15 + setae al + add esp, 4 + pop esi + pop ebp + ret +LBB5_1: + call SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0) + jmp LBB5_2 + + .section __DATA,__const + .p2align 2, 0x0 +l_anon.[ID].0: + .asciz "\000\000\000\000\004\000\000\000\004\000\000" + .long SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0) + .long SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0) + + .section __TEXT,__const +l_anon.[ID].1: + .ascii "$RUSTC/library/std/src/sync/once.rs" + + .section __DATA,__const + .p2align 2, 0x0 +l_anon.[ID].2: + .long l_anon.[ID].1 + .asciz "p\000\000\000\331\000\000\000\024\000\000" + + .p2align 2, 0x0 +l_anon.[ID].3: + .long l_anon.[ID].1 + .asciz "p\000\000\000\331\000\000\0001\000\000" + + .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..59fc34f8b --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/apple-x86_64.s @@ -0,0 +1,147 @@ + .section __TEXT,__text,regular,pure_instructions + .intel_syntax noprefix +SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0): + mov rdi, qword ptr [rip + SYM(objc2::__macro_helpers::os_version::apple::current_version::CURRENT_VERSION::GENERATED_ID, 0)@GOTPCREL] + mov rax, qword ptr [rdi] + cmp rax, 3 + jne LBB0_1 + ret +LBB0_1: + push rbp + mov rbp, rsp + sub rsp, 32 + lea rax, [rdi + 8] + lea rcx, [rbp - 32] + mov qword ptr [rcx], rax + lea rax, [rbp - 1] + mov qword ptr [rcx + 8], rax + lea rdx, [rbp - 16] + mov qword ptr [rdx], rcx + lea rcx, [rip + l_anon.[ID].0] + lea r8, [rip + l_anon.[ID].2] + push 1 + pop rsi + call SYM(std::sys::sync::once::queue::Once::call::GENERATED_ID, 0) + add rsp, 32 + pop rbp + ret + + .p2align 4, 0x90 +SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0): + push rbp + mov rbp, rsp + push rbx + push rax + mov rax, qword ptr [rdi] + mov rbx, qword ptr [rax] + mov qword ptr [rax], 0 + test rbx, rbx + je LBB1_5 + call SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + test ax, ax + je LBB1_2 + shr rax, 16 + jmp LBB1_4 +LBB1_2: + call SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB1_4: + mov dword ptr [rbx], eax + add rsp, 8 + pop rbx + pop rbp + ret +LBB1_5: + lea rdi, [rip + l_anon.[ID].3] + call SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .p2align 4, 0x90 +SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0): + push rbp + mov rbp, rsp + push rbx + push rax + mov rax, qword ptr [rdi] + mov rbx, qword ptr [rax] + mov qword ptr [rax], 0 + test rbx, rbx + je LBB2_5 + call SYM(objc2::__macro_helpers::os_version::apple::version_from_sysctl::GENERATED_ID, 0) + test ax, ax + je LBB2_2 + shr rax, 16 + jmp LBB2_4 +LBB2_2: + call SYM(objc2::__macro_helpers::os_version::apple::version_from_plist::GENERATED_ID, 0) +LBB2_4: + mov dword ptr [rbx], eax + add rsp, 8 + pop rbx + pop rbp + ret +LBB2_5: + lea rdi, [rip + l_anon.[ID].3] + call SYM(core::option::unwrap_failed::GENERATED_ID, 0) + + .globl _always + .p2align 4, 0x90 +_always: + push rbp + mov rbp, rsp + mov al, 1 + 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 rax, qword ptr [rbx] + cmp rax, 3 + jne LBB5_1 +LBB5_2: + cmp word ptr [rbx + 10], 15 + setae al + add rsp, 8 + pop rbx + pop rbp + ret +LBB5_1: + call SYM(>::initialize::<>::get_or_init::{closure#0}, !>, 0) + jmp LBB5_2 + + .section __DATA,__const + .p2align 3, 0x0 +l_anon.[ID].0: + .asciz "\000\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\b\000\000\000\000\000\000" + .quad SYM(<::call_once_force<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0} as core[CRATE_ID]::ops::function::FnOnce<(&std[CRATE_ID]::sync::once::OnceState,)>>::call_once::{shim:vtable#0}, 0) + .quad SYM(::call_once_force::<>::initialize<>::get_or_init::{closure#0}, !>::{closure#0}>::{closure#0}, 0) + + .section __TEXT,__const +l_anon.[ID].1: + .ascii "$RUSTC/library/std/src/sync/once.rs" + + .section __DATA,__const + .p2align 3, 0x0 +l_anon.[ID].2: + .quad l_anon.[ID].1 + .asciz "p\000\000\000\000\000\000\000\331\000\000\000\024\000\000" + + .p2align 3, 0x0 +l_anon.[ID].3: + .quad l_anon.[ID].1 + .asciz "p\000\000\000\000\000\000\000\331\000\000\0001\000\000" + +.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..83fd37fc6 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/gnustep-x86.s @@ -0,0 +1,33 @@ + .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.low,"ax",@progbits + .globl low + .p2align 4, 0x90 + .type low,@function +low: + mov al, 1 + ret +.Lfunc_end1: + .size low, .Lfunc_end1-low + + .section .text.high,"ax",@progbits + .globl high + .p2align 4, 0x90 + .type high,@function +high: + mov al, 1 + ret +.Lfunc_end2: + .size high, .Lfunc_end2-high + + .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..83fd37fc6 --- /dev/null +++ b/crates/test-assembly/crates/test_available/expected/gnustep-x86_64.s @@ -0,0 +1,33 @@ + .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.low,"ax",@progbits + .globl low + .p2align 4, 0x90 + .type low,@function +low: + mov al, 1 + ret +.Lfunc_end1: + .size low, .Lfunc_end1-low + + .section .text.high,"ax",@progbits + .globl high + .p2align 4, 0x90 + .type high,@function +high: + mov al, 1 + ret +.Lfunc_end2: + .size high, .Lfunc_end2-high + + .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..9aef195ad --- /dev/null +++ b/crates/test-assembly/crates/test_available/lib.rs @@ -0,0 +1,34 @@ +//! 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 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, + .. + ) +} 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..83e94e228 --- /dev/null +++ b/crates/test-ui/ui/available_invalid.stderr @@ -0,0 +1,55 @@ +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 `=` + --> $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 `=` + --> $WORKSPACE/crates/objc2/src/macros/available.rs + | + | $os:ident = $major:literal $(. $minor:literal $(. $patch:literal)?)? + | ^ diff --git a/crates/test-ui/ui/available_missing_current_os.rs b/crates/test-ui/ui/available_missing_current_os.rs new file mode 100644 index 000000000..892300e17 --- /dev/null +++ b/crates/test-ui/ui/available_missing_current_os.rs @@ -0,0 +1,10 @@ +//! Missing current OS in `available!` macro. +use objc2::available; + +fn main() { + // Disallow + available!(ios = 1.2); + + // Allow + available!(ios = 1.2, ..); +} diff --git a/crates/test-ui/ui/available_missing_current_os.stderr b/crates/test-ui/ui/available_missing_current_os.stderr new file mode 100644 index 000000000..873eaec24 --- /dev/null +++ b/crates/test-ui/ui/available_missing_current_os.stderr @@ -0,0 +1,7 @@ +error[E0063]: missing field `macos` in initializer of `AvailableVersion` + --> ui/available_missing_current_os.rs + | + | available!(ios = 1.2); + | ^^^^^^^^^^^^^^^^^^^^^ missing `macos` + | + = note: this error originates in the macro `$crate::__available_fields` which comes from the expansion of 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