|
| 1 | +//! Runtime version checking ABI for other compilers. |
| 2 | +//! |
| 3 | +//! The symbols in this file are useful for us to expose to allow linking code written in the |
| 4 | +//! following languages when using their version checking functionality: |
| 5 | +//! - Objective-C's `@available`. |
| 6 | +//! - Clang's `__builtin_available` macro. |
| 7 | +//! - Swift's `#available`, |
| 8 | +//! |
| 9 | +//! The original discussion of this feature can be found at: |
| 10 | +//! - <https://lists.llvm.org/pipermail/cfe-dev/2016-July/049851.html> |
| 11 | +//! - <https://reviews.llvm.org/D27827> |
| 12 | +//! - <https://reviews.llvm.org/D30136> |
| 13 | +//! |
| 14 | +//! The implementation of these is a bit weird, since they're actually implemented in `compiler-rt`: |
| 15 | +//! <https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/compiler-rt/lib/builtins/os_version_check.c> |
| 16 | +//! |
| 17 | +//! But they probably should've been part of `libSystem.dylib`, both because they link to symbols |
| 18 | +//! from that, and because their implementation is quite complex, using allocation, environment |
| 19 | +//! variables, file access and dynamic library loading (and emitting all of this into every binary). |
| 20 | +//! |
| 21 | +//! The reason why Apple chose to not do that originally is lost to the sands of time, but a good |
| 22 | +//! reason would be that implementing it as part of `compiler-rt` allowed them to back-deploy this |
| 23 | +//! to older OSes immediately. |
| 24 | +//! |
| 25 | +//! In Rust's case, while we may provide a feature similar to `@available` in the future, we will |
| 26 | +//! probably do so as a macro exposed by `std` (and not as a compiler builtin). So implementing this |
| 27 | +//! in `std` makes sense, since then we can implement it using `std` utilities, and we can avoid |
| 28 | +//! having `compiler-builtins` depend on `libSystem.dylib`. |
| 29 | +//! |
| 30 | +//! This does mean that users that attempt to link Objective-C code _and_ use `#![no_std]` in all |
| 31 | +//! their crates may get a linker error because these symbols are missing. Using `no_std` is quite |
| 32 | +//! uncommon on Apple systems though, so it's probably fine to not support this use-case. |
| 33 | +//! |
| 34 | +//! The workaround would be to link `libclang_rt.osx.a`, or otherwise use Clang's `compiler-rt`. |
| 35 | +//! |
| 36 | +//! See also discussion in <https://github.com/rust-lang/compiler-builtins/pull/794>. |
| 37 | +//! |
| 38 | +//! --- |
| 39 | +//! |
| 40 | +//! NOTE: Since macOS 10.15, `libSystem.dylib` _has_ actually provided the undocumented |
| 41 | +//! `_availability_version_check` via `libxpc` for doing the version lookup (zippered, which is why |
| 42 | +//! it requires a platform parameter to differentiate between macOS and Mac Catalyst), though it's |
| 43 | +//! usage may be a bit dangerous, see: |
| 44 | +//! - https://reviews.llvm.org/D150397 |
| 45 | +//! - https://github.com/llvm/llvm-project/issues/64227 |
| 46 | +//! |
| 47 | +//! Besides, we'd need to implement the version lookup via. PList to support older versions anyhow, |
| 48 | +//! so we might as well use that everywhere (since it can also be optimized more after inlining). |
| 49 | +
|
| 50 | +#![allow(non_snake_case)] |
| 51 | + |
| 52 | +use super::{current_version, pack_os_version}; |
| 53 | + |
| 54 | +/// Whether the current platform's OS version is higher than or equal to the given version. |
| 55 | +/// |
| 56 | +/// The first argument is the _base_ Mach-O platform (i.e. `PLATFORM_MACOS`, `PLATFORM_IOS`, etc., |
| 57 | +/// but not `PLATFORM_IOSSIMULATOR` or `PLATFORM_MACCATALYST`) of the invoking binary. |
| 58 | +// |
| 59 | +// Arguments are specified statically by Clang. Inlining with LTO should allow the versions to be |
| 60 | +// combined into a single `u32`, which should make comparisons faster, and should make the |
| 61 | +// `BASE_TARGET_PLATFORM` check a no-op. |
| 62 | +#[inline] |
| 63 | +// SAFETY: The signature is the same as what Clang expects, and we export weakly to allow linking |
| 64 | +// both this and `libclang_rt.*.a`, similar to how `compiler-builtins` does it: |
| 65 | +// https://github.com/rust-lang/compiler-builtins/blob/0.1.113/src/macros.rs#L494 |
| 66 | +#[cfg_attr(not(feature = "compiler-builtins-mangled-names"), unsafe(no_mangle), linkage = "weak")] |
| 67 | +// extern "C" is correct, Clang assumes the function cannot unwind: |
| 68 | +// https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/clang/lib/CodeGen/CGObjC.cpp#L3980 |
| 69 | +// |
| 70 | +// If an error happens in this, we instead abort the process. |
| 71 | +pub(super) extern "C" fn __isPlatformVersionAtLeast( |
| 72 | + platform: i32, |
| 73 | + major: u32, |
| 74 | + minor: u32, |
| 75 | + subminor: u32, |
| 76 | +) -> i32 { |
| 77 | + let version = pack_os_version(major as u16, minor as u8, subminor as u8); |
| 78 | + |
| 79 | + // Mac Catalyst is a technology that allows macOS to run in a different "mode" that closely |
| 80 | + // resembles iOS (and has iOS libraries like UIKit available). |
| 81 | + // |
| 82 | + // (Apple has added a "Designed for iPad" mode later on that allows running iOS apps |
| 83 | + // natively, but we don't need to think too much about those, since they link to |
| 84 | + // iOS-specific system binaries as well). |
| 85 | + // |
| 86 | + // To support Mac Catalyst, Apple has the concept of a "zippered" binary, which is a single |
| 87 | + // binary that can be run on both macOS and Mac Catalyst (has two `LC_BUILD_VERSION` Mach-O |
| 88 | + // commands, one set to `PLATFORM_MACOS` and one to `PLATFORM_MACCATALYST`). |
| 89 | + // |
| 90 | + // Most system libraries are zippered, which allows re-use across macOS and Mac Catalyst. |
| 91 | + // This includes the `libclang_rt.osx.a` shipped with Xcode! This means that `compiler-rt` |
| 92 | + // can't statically know whether it's compiled for macOS or Mac Catalyst, and thus this new |
| 93 | + // API (which replaces `__isOSVersionAtLeast`) is needed. |
| 94 | + // |
| 95 | + // In short: |
| 96 | + // normal binary calls normal compiler-rt --> `__isOSVersionAtLeast` was enough |
| 97 | + // normal binary calls zippered compiler-rt --> `__isPlatformVersionAtLeast` required |
| 98 | + // zippered binary calls zippered compiler-rt --> `__isPlatformOrVariantPlatformVersionAtLeast` called |
| 99 | + |
| 100 | + // FIXME(madsmtm): `rustc` doesn't support zippered binaries yet, see rust-lang/rust#131216. |
| 101 | + // But once it does, we need the pre-compiled `std` shipped with rustup to be zippered, and thus |
| 102 | + // we also need to handle the `platform` difference here: |
| 103 | + // |
| 104 | + // if cfg!(target_os = "macos") && platform == 2 /* PLATFORM_IOS */ && cfg!(zippered) { |
| 105 | + // return (version.to_u32() <= current_ios_version()) as i32; |
| 106 | + // } |
| 107 | + // |
| 108 | + // `__isPlatformOrVariantPlatformVersionAtLeast` would also need to be implemented. |
| 109 | + |
| 110 | + // The base Mach-O platform for the current target. |
| 111 | + const BASE_TARGET_PLATFORM: i32 = if cfg!(target_os = "macos") { |
| 112 | + 1 // PLATFORM_MACOS |
| 113 | + } else if cfg!(target_os = "ios") { |
| 114 | + 2 // PLATFORM_IOS |
| 115 | + } else if cfg!(target_os = "tvos") { |
| 116 | + 3 // PLATFORM_TVOS |
| 117 | + } else if cfg!(target_os = "watchos") { |
| 118 | + 4 // PLATFORM_WATCHOS |
| 119 | + } else if cfg!(target_os = "visionos") { |
| 120 | + 11 // PLATFORM_VISIONOS |
| 121 | + } else { |
| 122 | + 0 // PLATFORM_UNKNOWN |
| 123 | + }; |
| 124 | + debug_assert!( |
| 125 | + platform == BASE_TARGET_PLATFORM, |
| 126 | + "invalid platform provided to __isPlatformVersionAtLeast" |
| 127 | + ); |
| 128 | + |
| 129 | + (version <= current_version()) as i32 |
| 130 | +} |
| 131 | + |
| 132 | +/// Old entry point for availability. Used when compiling with older Clang versions. |
| 133 | +#[inline] |
| 134 | +// SAFETY: Same as for `__isPlatformVersionAtLeast`. |
| 135 | +#[cfg_attr(not(feature = "compiler-builtins-mangled-names"), unsafe(no_mangle), linkage = "weak")] |
| 136 | +pub(super) extern "C" fn __isOSVersionAtLeast(major: u32, minor: u32, subminor: u32) -> i32 { |
| 137 | + let version = pack_os_version(major as u16, minor as u8, subminor as u8); |
| 138 | + (version <= current_version()) as i32 |
| 139 | +} |
0 commit comments