Skip to content

Commit ea9a6dd

Browse files
Auto merge of #138944 - madsmtm:apple_os_version_check, r=<try>
Add `__isPlatformVersionAtLeast` and `__isOSVersionAtLeast` symbols ## Motivation When Objective-C code uses ``@available(...)`,` Clang inserts a call to [`__isPlatformVersionAtLeast`](https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/compiler-rt/lib/builtins/os_version_check.c#L276) (`__isOSVersionAtLeast` in older Clang versions). These symbols not being available sometimes ends up causing linker errors. See the new test `tests/run-make/apple-c-available-links` for a minimal reproducer. The workaround is to link `libclang_rt.osx.a`, see e.g. alexcrichton/curl-rust#279. But that's very difficult for users to figure out (and the backreferences to that issue indicates that people are still running into this in their own projects every so often). For another recent example, this is preventing `rustc` from using LLVM assertions on macOS, see #62592 (comment) and #134275 (comment). It is also a blocker for [setting the correct minimum OS version in `cc-rs`](#136113), since fixing this in `cc-rs` might end up introducing linker errors in places where we weren't before (by default, if using e.g. ``@available(macos` 10.15, *)`, the symbol usually happens to be left out, since `clang` defaults to compiling for the host macOS version, and thus things _seem_ to work - but the availability check actually compiles down to nothing, which is a huge correctness footgun for running on older OSes). (My super secret evil agenda is also to expose some variant of ``@available`` in Rust's `std` after rust-lang/rfcs#3750 progresses further, will probably file an ACP for this later. But I believe this PR has value regardless of those future plans, since we'd be making C/Objective-C/Swift interop easier). ## Solution Implement `__isPlatformVersionAtLeast` and `__isOSVersionAtLeast` as part of the "public ABI" that `std` exposes. **This is insta-stable**, in the same sense that additions to `compiler-builtins` are insta-stable, though the availability of these symbols can probably be considered a "quality of implementation" detail rather than a stable promise. I originally proposed to implement this in `compiler-builtins`, see rust-lang/compiler-builtins#794, but we discussed moving it to `std` instead ([Zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/219381-t-libs/topic/Provide.20.60__isPlatformVersionAtLeast.60.20in.20.60std.60.3F/with/507880717)), which makes the implementation substantially simpler, and we avoid gnarly issues with requiring the user to link `libSystem.dylib` (since `std` unconditionally does that). Note that this does not solve the linker errors for (pure) `#![no_std]` users, but that's _probably_ fine, if you are using ``@available`` to test the OS version on Apple platforms, you're likely also using `std` (and it is still possible to work around by linking `libclang_rt.*.a`). A thing to note about the implementation, I've choosen to stray a bit from LLVM's upstream implementation, and not use `_availability_version_check` since [it has problems when compiling with an older SDK](llvm/llvm-project#64227). Instead, we use `sysctl kern.osproductversion` when available to still avoid the costly PList lookup in most cases, but still with a fall back to the PList lookup when that is not available (with the PList fallback being is similar to LLVM's implementation). ## Testing Apple has a lot of different "modes" that they can run binaries in, which can be a bit difficult to find your bearings in, but I've tried to be as thorough as I could in testing them all. Tested using roughly the equivalent of `./x test library/std -- platform_version` on the following configurations: - macOS 14.7.3 on a Macbook Pro M2 - `aarch64-apple-darwin` - `x86_64-apple-darwin` (under Rosetta) - `aarch64-apple-ios-macabi` - `x86_64-apple-ios-macabi` (under Rosetta) - `aarch64-apple-ios` (using Xcode's "Designed for iPad" setting) - `aarch64-apple-ios-sim` (in iOS Simulator, as iPhone with iOS 17.5) - `aarch64-apple-ios-sim` (in iOS Simulator, as iPad with iOS 18.2) - `aarch64-apple-tvos-sim` (in tvOS Simulator) - `aarch64-apple-watchos-sim` (in watchOS Simulator) - `aarch64-apple-ios-sim` (in visionOS simulator, using Xcode's "Designed for iPad" setting) - `aarch64-apple-visionos-sim` (in visionOS Simulator) - macOS 15.3.1 VM - `aarch64-apple-darwin` - `aarch64-apple-ios-macabi` - macOS 10.12.6 on an Intel Macbook from 2013 - `x86_64-apple-darwin` - `i686-apple-darwin` - `x86_64-apple-ios` (in iOS Simulator) - iOS 9.3.6 on a 1st generation iPad Mini - `armv7-apple-ios` with an older compiler Along with manually inspecting the output of `version_from_sysctl()` and `version_from_plist()`, and verifying that they actually match what's expected. I believe the only real omissions here would be: - `aarch64-apple-ios` on a newer iPhone that has `sysctl` available (iOS 11.4 or above). - `aarch64-apple-ios` on a Vision Pro using Xcode's "Designed for iPad" setting. But I don't have the hardware available to test those. `@rustbot` label O-apple A-linkage -T-compiler -A-meta -A-run-make try-job: aarch64-apple try-job: x86_64-apple*
2 parents 015c777 + 779ac2f commit ea9a6dd

File tree

12 files changed

+1133
-4
lines changed

12 files changed

+1133
-4
lines changed

compiler/rustc_symbol_mangling/src/v0.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,16 @@ pub(super) fn mangle<'tcx>(
8282
}
8383

8484
pub fn mangle_internal_symbol<'tcx>(tcx: TyCtxt<'tcx>, item_name: &str) -> String {
85-
if item_name == "rust_eh_personality" {
85+
match item_name {
8686
// rust_eh_personality must not be renamed as LLVM hard-codes the name
87-
return "rust_eh_personality".to_owned();
88-
} else if item_name == "__rust_no_alloc_shim_is_unstable" {
87+
"rust_eh_personality" => return item_name.to_owned(),
8988
// Temporary back compat hack to give people the chance to migrate to
9089
// include #[rustc_std_internal_symbol].
91-
return "__rust_no_alloc_shim_is_unstable".to_owned();
90+
"__rust_no_alloc_shim_is_unstable" => return item_name.to_owned(),
91+
// Apple availability symbols need to not be mangled to be usable by
92+
// C/Objective-C code.
93+
"__isPlatformVersionAtLeast" | "__isOSVersionAtLeast" => return item_name.to_owned(),
94+
_ => {}
9295
}
9396

9497
let prefix = "_R";

library/std/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@
347347
#![feature(hasher_prefixfree_extras)]
348348
#![feature(hashmap_internals)]
349349
#![feature(hint_must_use)]
350+
#![feature(int_from_ascii)]
350351
#![feature(ip)]
351352
#![feature(lazy_get)]
352353
#![feature(maybe_uninit_slice)]
@@ -362,6 +363,7 @@
362363
#![feature(slice_internals)]
363364
#![feature(slice_ptr_get)]
364365
#![feature(slice_range)]
366+
#![feature(slice_split_once)]
365367
#![feature(std_internals)]
366368
#![feature(str_internals)]
367369
#![feature(strict_provenance_atomic_ptr)]

library/std/src/sys/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod io;
2121
pub mod net;
2222
pub mod os_str;
2323
pub mod path;
24+
pub mod platform_version;
2425
pub mod process;
2526
pub mod random;
2627
pub mod stdio;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//! Minimal utilities for interfacing with a dynamically loaded CoreFoundation.
2+
#![allow(non_snake_case, non_upper_case_globals)]
3+
use super::root_relative;
4+
use crate::ffi::{CStr, c_char, c_void};
5+
use crate::ptr::null_mut;
6+
use crate::sys::common::small_c_string::run_path_with_cstr;
7+
8+
// MacTypes.h
9+
pub(super) type Boolean = u8;
10+
// CoreFoundation/CFBase.h
11+
pub(super) type CFTypeID = usize;
12+
pub(super) type CFOptionFlags = usize;
13+
pub(super) type CFIndex = isize;
14+
pub(super) type CFTypeRef = *mut c_void;
15+
pub(super) type CFAllocatorRef = CFTypeRef;
16+
pub(super) const kCFAllocatorDefault: CFAllocatorRef = null_mut();
17+
// CoreFoundation/CFError.h
18+
pub(super) type CFErrorRef = CFTypeRef;
19+
// CoreFoundation/CFData.h
20+
pub(super) type CFDataRef = CFTypeRef;
21+
// CoreFoundation/CFPropertyList.h
22+
pub(super) const kCFPropertyListImmutable: CFOptionFlags = 0;
23+
pub(super) type CFPropertyListFormat = CFIndex;
24+
pub(super) type CFPropertyListRef = CFTypeRef;
25+
// CoreFoundation/CFString.h
26+
pub(super) type CFStringRef = CFTypeRef;
27+
pub(super) type CFStringEncoding = u32;
28+
pub(super) const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
29+
// CoreFoundation/CFDictionary.h
30+
pub(super) type CFDictionaryRef = CFTypeRef;
31+
32+
/// An open handle to the dynamically loaded CoreFoundation framework.
33+
///
34+
/// This is `dlopen`ed, and later `dlclose`d. This is done to try to avoid
35+
/// "leaking" the CoreFoundation symbols to the rest of the user's binary if
36+
/// they decided to not link CoreFoundation themselves.
37+
///
38+
/// It is also faster to look up symbols directly via this handle than with
39+
/// `RTLD_DEFAULT`.
40+
pub(super) struct CFHandle(*mut c_void);
41+
42+
macro_rules! dlsym_fn {
43+
(
44+
unsafe fn $name:ident($($param:ident: $param_ty:ty),* $(,)?) $(-> $ret:ty)?;
45+
) => {
46+
pub(super) unsafe fn $name(&self, $($param: $param_ty),*) $(-> $ret)? {
47+
let ptr = unsafe {
48+
libc::dlsym(
49+
self.0,
50+
concat!(stringify!($name), '\0').as_bytes().as_ptr().cast(),
51+
)
52+
};
53+
if ptr.is_null() {
54+
let err = unsafe { CStr::from_ptr(libc::dlerror()) };
55+
panic!("could not find function {}: {err:?}", stringify!($name));
56+
}
57+
58+
// SAFETY: Just checked that the symbol isn't NULL, and macro invoker verifies that
59+
// the signature is correct.
60+
let fnptr = unsafe {
61+
crate::mem::transmute::<
62+
*mut c_void,
63+
unsafe extern "C" fn($($param_ty),*) $(-> $ret)?,
64+
>(ptr)
65+
};
66+
67+
// SAFETY: Upheld by caller.
68+
unsafe { fnptr($($param),*) }
69+
}
70+
};
71+
}
72+
73+
impl CFHandle {
74+
/// Link to the CoreFoundation dylib, and look up symbols from that.
75+
pub(super) fn new() -> Self {
76+
// We explicitly use non-versioned path here, to allow this to work on older iOS devices.
77+
let cf_path =
78+
root_relative("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
79+
80+
let handle = run_path_with_cstr(&cf_path, &|path| unsafe {
81+
Ok(libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL))
82+
})
83+
.expect("failed allocating string");
84+
85+
if handle.is_null() {
86+
let err = unsafe { CStr::from_ptr(libc::dlerror()) };
87+
panic!("could not open CoreFoundation.framework: {err:?}");
88+
}
89+
90+
Self(handle)
91+
}
92+
93+
pub(super) fn kCFAllocatorNull(&self) -> CFAllocatorRef {
94+
// Available: in all CF versions.
95+
let static_ptr = unsafe { libc::dlsym(self.0, c"kCFAllocatorNull".as_ptr()) };
96+
if static_ptr.is_null() {
97+
let err = unsafe { CStr::from_ptr(libc::dlerror()) };
98+
panic!("could not find kCFAllocatorNull: {err:?}");
99+
}
100+
unsafe { *static_ptr.cast() }
101+
}
102+
103+
// CoreFoundation/CFBase.h
104+
dlsym_fn!(
105+
// Available: in all CF versions.
106+
unsafe fn CFRelease(cf: CFTypeRef);
107+
);
108+
dlsym_fn!(
109+
// Available: in all CF versions.
110+
unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
111+
);
112+
113+
// CoreFoundation/CFData.h
114+
dlsym_fn!(
115+
// Available: in all CF versions.
116+
unsafe fn CFDataCreateWithBytesNoCopy(
117+
allocator: CFAllocatorRef,
118+
bytes: *const u8,
119+
length: CFIndex,
120+
bytes_deallocator: CFAllocatorRef,
121+
) -> CFDataRef;
122+
);
123+
124+
// CoreFoundation/CFPropertyList.h
125+
dlsym_fn!(
126+
// Available: since macOS 10.6.
127+
unsafe fn CFPropertyListCreateWithData(
128+
allocator: CFAllocatorRef,
129+
data: CFDataRef,
130+
options: CFOptionFlags,
131+
format: *mut CFPropertyListFormat,
132+
error: *mut CFErrorRef,
133+
) -> CFPropertyListRef;
134+
);
135+
136+
// CoreFoundation/CFString.h
137+
dlsym_fn!(
138+
// Available: in all CF versions.
139+
unsafe fn CFStringGetTypeID() -> CFTypeID;
140+
);
141+
dlsym_fn!(
142+
// Available: in all CF versions.
143+
unsafe fn CFStringCreateWithCStringNoCopy(
144+
alloc: CFAllocatorRef,
145+
c_str: *const c_char,
146+
encoding: CFStringEncoding,
147+
contents_deallocator: CFAllocatorRef,
148+
) -> CFStringRef;
149+
);
150+
dlsym_fn!(
151+
// Available: in all CF versions.
152+
unsafe fn CFStringGetCString(
153+
the_string: CFStringRef,
154+
buffer: *mut c_char,
155+
buffer_size: CFIndex,
156+
encoding: CFStringEncoding,
157+
) -> Boolean;
158+
);
159+
160+
// CoreFoundation/CFDictionary.h
161+
dlsym_fn!(
162+
// Available: in all CF versions.
163+
unsafe fn CFDictionaryGetTypeID() -> CFTypeID;
164+
);
165+
dlsym_fn!(
166+
// Available: in all CF versions.
167+
unsafe fn CFDictionaryGetValue(
168+
the_dict: CFDictionaryRef,
169+
key: *const c_void,
170+
) -> *const c_void;
171+
);
172+
}
173+
174+
impl Drop for CFHandle {
175+
fn drop(&mut self) {
176+
// Ignore errors when closing. This is also what `libloading` does:
177+
// https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374
178+
let _ = unsafe { libc::dlclose(self.0) };
179+
}
180+
}

0 commit comments

Comments
 (0)