-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement __isPlatformVersionAtLeast
and __isOSVersionAtLeast
#794
Conversation
1e9e3cb
to
3425b27
Compare
I don't have perms to do: So gonna ping people from that notification group manually: |
1aed11c
to
8e9c8c4
Compare
8e9c8c4
to
ca9c40d
Compare
Slightly off-topic, I’ve also encountered issues when library need symbols from |
// Test internals | ||
|
||
#[path = "../../src/os_version_check/darwin_impl.rs"] | ||
#[allow(dead_code)] | ||
mod darwin_impl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit unsure about this, should I make it a unit-test inside src/os_version_check/darwin_impl.rs
instead of an integration test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unit tests unfortunately don't really work in compiler-builtins
. It is fine to just make everything public so you don't need to do the module path trick.
#[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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfamiliar with compiler-builtins
, so I can't tell if there's a better way of doing this kind of parsing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things are helped by this being specific to Apple platforms, so we already depend on libSystem.dylib
, and could use for example sscanf
. What do you prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think I'd prefer that myself, it's also what LLVM does (the manual parsing here was a remnant of my implementation in objc2
).
Will wait 'till I hear back, and then I'll probably use sscanf
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a closer look at the parsing in a bit, but it is probably better not to depend on sscanf
for the reason mentioned in #794 (comment).
Unsure, I haven't really looked into where Clang / Xcode has its runtime libraries for different archs. In any case, what you're discussing is probably more relevant to #296. |
|
||
#[cold] | ||
// Use `extern "C"` to abort on panic, allowing `current_version` to be free of panic handling. | ||
pub(super) extern "C" fn lookup_version() -> NonZero<OSVersion> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this extern C?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a micro-optimization, can remove it if you want.
Details: Optimally, we inline __isPlatformVersionAtLeast
and current_version
(requires LTO), and thus end up with a direct call to lookup_version
. By marking it extern "C"
, we force any panics from version_from_sysctl
/version_from_plist
to be turned into aborts (without having to deal with std::panic::catch_unwind
), which improves code-size (since the now-inlined caller doesn't have to emit unwind handling).
/// Panics if reading or parsing the version fails (or if the system was out of memory). | ||
/// | ||
/// We deliberately choose to panic, as having this silently return an invalid OS version would be | ||
/// impossible for a user to debug. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compiler-builtins may not panic as that requires libcore but compiler-builtins can't depend on any other crates as it is last on the liker cli. Instead you will have to abort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as it is last on the liker cli
Pretty sure Apple's linker doesn't require ordering in the same way as traditional ELF linkers?
I'll have to test it to be sure though, I'll see if I can build a custom toolchain with this branch of compiler-builtins
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if the linker accepts it, you run the risk of cyclic dependencies with the panic handler (as __isPlatformVersionAtLeast
panics, which calls the panic handler, which in turn calls a function that calls __isPlatformVersionAtLeast
).
// SAFETY: Same signatures as in `libc`. | ||
// | ||
// NOTE: We do not need to link these; that will be done by `std` by linking `libSystem` | ||
// (which is required on macOS/Darwin). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the user is building a no_std macOS application?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fairly sure even no_std
requires libSystem.dylib
(or libc.dylib
or similar, which exports many of the same symbols).
For example, compiling the following with rustc no_std.rs -Cpanic=abort
:
#![no_std]
#![no_main]
#[panic_handler]
fn handler(_info: &core::panic::PanicInfo<'_>) -> ! {
loop {}
}
#[no_mangle]
fn main() {}
Results in:
Undefined symbols for architecture arm64:
"dyld_stub_binder", referenced from:
<initial-undefines>
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
That is, fundamentally, building a Mach-O dylib (of which binaries are a subset) requires the dyld_stub_binder
symbol (I think it'd inserted by the linker?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait... I think I may be mistaken here.
Seems like using MACOSX_DEPLOYMENT_TARGET=12.0
doesn't require the symbol! So you're probably right that we'd need to add a #[link(name = "System")]
.
Could it make sense to provide this as a weak symbol in For similar reasons I am assuming LLVM would also prefer these to be This might be worth bringing up on Zulip, and/or just submitting an ACP to add a version of |
I'm not familiar enough with the linking constraints that To be clear, I'd be fine with requiring
It seems crazy to me too, but yes, it does require all of these.
Ah, yeah, thinking about it, I suspect they added it to Actually, does compiler built-ins in Clang that call
I've opened a Zulip thread, will submit an ACP for a version of |
This has to be tested but I think it should. Amanieu mentioned before that static+weak symbols may get picked over dynamic+strong, so ours might get favored. But that doesn't seem like a problem.
|
I have opened rust-lang/rust#138944 instead, will pursue that and close this for now, thanks for the input so far!
Turns out I was a little bit mistaken in my comment here, |
Motivation
When Objective-C code uses
@available(...)
, Clang inserts a call to__isPlatformVersionAtLeast
(__isOSVersionAtLeast
in older Clang versions). These symbols not being available incompiler-builtins
sometimes ends up causing linker errors.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 a recent example, this is preventing
rustc
from using LLVM assertions on macOS, see rust-lang/rust#62592 (comment) and rust-lang/rust#134275 (comment).Apart from linker errors above, it is also a blocker for setting the correct minimum OS version in
cc-rs
, which is a huge correctness footgun: By default, if using e.g.__builtin_available(macos 10.15, *)
, the symbol usually happens to be left out, sinceclang
defaults to compiling for the host macOS version, and thus things seem to work. But there's a reason why people write availability checks, which is that otherwise the code won't work on the older OS! But if we want to fix this incc-rs
, we might end up introducing linker errors in places where they weren't before.Example crate where this behaviour is illustrated
(Finally, I'll reveal that my super secret evil agenda is to expose some variant of
@available
/__builtin_available
in Rust'sstd
, and that'll probably be easier if the bulk of the implementation is already here incompiler-builtins
. But I believe adding this here has value regardless of those future plans).Solution
Implement
__isPlatformVersionAtLeast
and__isOSVersionAtLeast
, see the code comments for more details. Of particular note: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. Instead, we usesysctlbyname
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 (this fallback is similar to LLVM's implementation).I've also preferred to panic when hitting an unexpected state, instead of silently returning a wrong value, but am unsure what your policies are around this?
Testing
Tested using the equivalent of
cargo test --manifest-path testcrate/Cargo.toml --test os_version_check
on the following configurations: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)aarch64-apple-darwin
aarch64-apple-ios-macabi
x86_64-apple-darwin
i686-apple-darwin
x86_64-apple-ios
(in iOS Simulator)armv7-apple-ios
with an older compilerAlong with manually inspecting the output of
version_from_sysctl()
andversion_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 hassysctl
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.