Skip to content

Commit

Permalink
TMP
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Oct 3, 2024
1 parent 67a4acd commit eef14c9
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 2 deletions.
2 changes: 2 additions & 0 deletions crates/objc2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
384 changes: 384 additions & 0 deletions crates/objc2/src/__macro_helpers/availability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
//! Utilities for checking the runtime availability of APIs.
//!
//! TODO: Upstream some of this to `std`?
use core::cmp::Ordering;

/// The size of the fields here are limited by Mach-O's `LC_BUILD_VERSION`.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
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,
}

impl OSVersion {
/// Parse the version from a string at `const` time.
#[track_caller]
pub const fn from_str(version: &str) -> Self {
#[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)
}

let bytes = version.as_bytes();

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,
}
}

/// Look up the current version at runtime.
///
/// The version is cached for performance.
#[inline]
#[cfg(target_vendor = "apple")]
fn current() -> Self {
// This is mostly a re-implementation of:
// <https://github.com/llvm/llvm-project/blob/llvmorg-19.1.1/compiler-rt/lib/builtins/os_version_check.c>
//
// Note that this doesn't work with [zippered] `dylib`s yet, though
// that's probably fine, `rustc` doesn't support those either.
// [zippered]: https://github.com/nico/hack/blob/be6e1a6885a9d5179558b37a0b4c36bec9c4d377/notes/catalyst.md#building-a-zippered-dylib

// Rust does not yet support weak externs, and besides, I suspect it'd
// be dangerous to use, see:
// - https://reviews.llvm.org/D150397
// - https://github.com/llvm/llvm-project/issues/64227

// TODO
DEPLOYMENT_TARGET
}

#[inline]
#[cfg(not(target_vendor = "apple"))]
fn current() -> Self {
Self::default()
}

/// 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<Ordering> {
self.to_u32().partial_cmp(&other.to_u32())
}
}

/// The deployment target for the current OS.
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");

// GNUStep etc. don't have a concept of deployment target.
#[cfg(not(target_vendor = "apple"))]
let var = None;

if let Some(var) = var {
OSVersion::from_str(var)
} else {
// Default operating system version.
// See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L207-L215>
//
// Note that we cannot do as they suggest, and use
// `rustc --print=deployment-target`, as this has to work at
// `const` time.
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 {
// GNUStep etc. don't have a concept of deployment target.
(0, 0, 0)
};

// On certain targets it makes sense to raise the minimum OS version.
//
// See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L217-L231>
//
// Note that we cannot do all the same checks as `rustc` does, because
// `target_abi` is not in our MSRV (introduced in 1.78), and because
// we have no way of knowing if the architecture is `arm64e`.
let min = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
(11, 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,
}
}
};

/// 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, Default)]
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,
}

#[inline]
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;

// Assume that things are always available on GNUStep etc.
#[cfg(not(target_vendor = "apple"))]
let version = OSVersion::default();

// If the deployment target is high enough, the API is always available.
//
// This check should be optimized away at compile time.
if version <= DEPLOYMENT_TARGET {
return true;
}

// Otherwise, compare against the version at runtime.
version <= OSVersion::current()
}

#[cfg(test)]
mod tests {
use super::*;

use crate::__available_version;

#[track_caller]
fn check(expected: (u16, u8, u8), actual: OSVersion) {
assert_eq!(
actual,
OSVersion {
major: expected.0,
minor: expected.1,
patch: expected.2,
}
)
}

#[test]
fn test_parse() {
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]
#[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);
}
}
Loading

0 comments on commit eef14c9

Please sign in to comment.