Skip to content

RFC: cfg_os_version_min #3750

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

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c08880c
RFC: cfg_os_version_min
ChrisDenton Dec 27, 2024
d78f82b
Update RFC PR number
ChrisDenton Dec 27, 2024
58ae3de
Remove "in use"
ChrisDenton Jan 2, 2025
0b8f912
Update text/3750-cfg-os-version-min.md
ChrisDenton Jan 4, 2025
c3f33b8
Mention other possible syntax
ChrisDenton Jan 5, 2025
6e2245c
Update prior art to include how C does it
ChrisDenton Jan 5, 2025
35dc984
Update motivation and guide
ChrisDenton Jan 27, 2025
5332c9b
Update reference level explanation
ChrisDenton Jan 27, 2025
859a733
Add a linting section
ChrisDenton Jan 27, 2025
6c7e0c6
Remove double "additional"
ChrisDenton Jan 27, 2025
16c6321
Add default supported target motivation
ChrisDenton Jan 27, 2025
68c00f8
Add a note about the standard library
ChrisDenton Jan 27, 2025
6b54296
Expand on the version string
ChrisDenton Jan 27, 2025
a9b5b74
Note that a higher baseline isn't yet supported
ChrisDenton Jan 27, 2025
b71051b
Fix some headings
ChrisDenton Jan 27, 2025
5cde247
Update 3750-cfg-os-version-min.md
ChrisDenton Jan 27, 2025
35896d1
Update 3750-cfg-os-version-min.md
ChrisDenton Jan 27, 2025
439fcea
Make future possibilities use bullet points
ChrisDenton Jan 29, 2025
89b604d
Clarify version string comparision
ChrisDenton Jan 29, 2025
c889a32
Update 3750-cfg-os-version-min.md
ChrisDenton Jan 29, 2025
d9715b4
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 9, 2025
35ee570
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 9, 2025
157b501
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 9, 2025
0edbe04
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 9, 2025
cd0e8a9
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 9, 2025
fb06a58
Update text/3750-cfg-os-version-min.md
ChrisDenton Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions text/3750-cfg-os-version-min.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
- Feature Name: `cfg_os_version_min`
- Start Date: 2024-12-27
- RFC PR: [rust-lang/rfcs#3750](https://github.com/rust-lang/rfcs/pull/3750)
- Rust Issue: [rust-lang/rust#136866](https://github.com/rust-lang/rust/issues/136866)

# Summary
[summary]: #summary

A new `cfg` predicate `os_version_min` that allows users to declare the minimum primary (target-defined) API level required/supported by a block.
E.g. `cfg!(os_version_min("windows", "6.1.7600"))` would match Windows version >= 6.1.7600.

# Motivation
[motivation]: #motivation

Operating systems and their libraries are continually advancing, adding and sometimes removing APIs or otherwise changing behaviour.
Versioning of APIs is common so that developers can target the set of APIs they support.
Crates, including the standard library, must account for various API version requirements for the crate to be able to run.
Rust currently has no mechanism for crates to compile different code (or to gracefully fail to compile) depending on the minimum targeted API version.
This leads to the following issues:

* Relying on dynamic detection of API support has a runtime cost.
The standard library often performs [dynamic API detection](https://github.com/rust-lang/rust/blob/f283d3f02cf3ed261a519afe05cde9e23d1d9278/library/std/src/sys/windows/compat.rs) falling back to older (and less ideal) APIs or forgoing entire features when a certain API is not available.
For example, the [current `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) has a Windows 7 fallback. Users who only ever intend to run their code on newer versions of Windows will still pay a runtime cost for this dynamic API detection.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Linking a 5 year old commit cannot really be called "current" anymore ;). It's a fine example though, just wish it was more honest. Maybe:

Suggested change
For example, the [current `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) has a Windows 7 fallback. Users who only ever intend to run their code on newer versions of Windows will still pay a runtime cost for this dynamic API detection.
For example, when the standard library still supported Windows 7 by default, [the `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) had a Windows 7 fallback. Users who only ever intended to run their code on newer versions of Windows still paid a runtime cost for this dynamic API detection.

Providing a mechanism for specifying which minimum API version the user cares about, allows for statically specifying which APIs a binary can use.
* Certain features cannot be dynamically detected and thus limit possible implementations.
The libc crate must use [a raw syscall on Android for `accept4`](https://github.com/rust-lang/libc/pull/1968), because this was only exposed in libc in version 21 of the Android API.
Additionally libstd must dynamically load `signal` for all versions of Android despite it being required only for versions 19 and below.
In the future there might be similar changes where there is no way to implement a solution for older versions.
* Trying to compile code with an implicit dependency on a API version greater than what is supported by the target platform leads to linker errors.
For example, the `x86_64-pc-windows-msvc` target's rustc implementation requires `SetThreadErrorMode` which was introduced in Windows 7.
This means trying to build the compiler on older versions of Windows will fail with [a less than helpful linker error](https://github.com/rust-lang/rust/issues/35471).
* Bumping the minimum supported version of a platform in Rust is a large endeavour.
By adding this feature, we enable [rustc to more gradually raise the supported version](https://github.com/rust-lang/rust/pull/104385#issuecomment-1453520239) or to have more "levels" of version support.
This would allow for having the "default" supported target be higher than the "minimum" supported target.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Rust targets are often thought of as monoliths.
The thought is that if you compile a binary for that target, that binary should be able to run on any system that fits that target's description.
However, this is not actually true.
For example, when compiling for `x86_64-pc-windows-msvc` and linking with the standard library, my binary has implicitly taken a dependency on a set of APIs that Windows exposes for certain functionality.
If I try to run my binary on older systems that do not have those APIs, then my binary will fail to run.
When compiling for a certain target, you are therefore declaring a dependency on a minimum target API version that you rely on for your binary to run.

Each standard library target uses a sensible minimum API version. for `x86_64-pc-windows-msvc` the minimum API version is "10.0.10240" which corresponds to Windows 10's initial release.
For `x86_64-win7-pc-windows-msvc` the minimum API version is "6.1.7600" which corresponds to Windows 7.
However, inferring the API version from the target name isn't ideal especially as it can change over time.

Instead you use the `os_version_min` predicates to specify the minimum API levels of various parts of the operating system. For example:

* `os_version_min("windows", <string>)` would test the [minimum build version](https://gaijin.at/en/infos/windows-version-numbers) of Windows.
* `os_version_min("libc", <string>)` would test the version of libc.
* `os_version_min("kernel", <string>)` would test the version of the kernel.

Let’s use `os_version_min("windows", …)` for a simple example.

```rust
pub fn random_u64() -> u64 {
let mut rand = 0_u64.to_ne_bytes();
if cfg!(os_version_min("windows", "10.0.10240")) {
// For an API version greater or equal to Windows 10, we use `ProcessPrng`
unsafe { ProcessPrng(rand.as_mut_ptr(), rand.len()) };
} else {
// Otherwise we fallback to `RtlGenRandom`
unsafe { RtlGenRandom(rand.as_mut_ptr().cast(), rand.len() as u32) };
}
u64::from_ne_bytes(rand)
}
```

A more involved example would be to attempt to dynamically load the symbol.
On macOS we use weak linking to do this:

```rust
// Always available under these conditions.
#[cfg(any(
os_version_min("macos", "11.0"),
os_version_min("ios", "14.0"),
os_version_min("tvos", "14.0"),
os_version_min("watchos", "7.0"),
os_version_min("visionos", "1.0")
))]
let preadv = {
extern "C" {
fn preadv(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize;
}
Some(preadv)
};

// Otherwise `preadv` needs to be weakly linked.
// We do that using a `weak!` macro, defined elsewhere.
#[cfg(not(any(
os_version_min("macos", "11.0"),
os_version_min("ios", "14.0"),
os_version_min("tvos", "14.0"),
os_version_min("watchos", "7.0"),
os_version_min("visionos", "1.0")
)))]
weak!(fn preadv(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize);

if let Some(preadv) = preadv {
preadv(...) // Use preadv, it's available
} else {
// ... fallback impl
}
```

# Reference-level explanation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to discuss here what happens when you link libraries compiled with different (even though we're not specifying how the user, we will need a mechanism for it at some point)

Specifically, it'd be nice to talk about the pre-compiled standard library, and how it effectively becomes a requirement to use -Zbuild-std if the user wants to enable this kind of stuff for the standard library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly also interesting is soundness concerns (what happens if I use a standard library compiled with a newer version of glibc/Windows APIs/macOS APIs, while I link with a binary compiled for older APIs?).

I don't think there are any soundness concerns, at least not on Apple platforms (the dynamic linker will simply fail to work if using an API for a newer system), but it's important that we're certain of this (and that function pointers for example aren't simply replaced by NULL if loaded on an older OS).

Copy link
Member Author

@ChrisDenton ChrisDenton Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There aren't unsoundness issues for Windows. Either a dll or symbol is available or it isn't and there's an error. EDIT: I'm being told that attempting to use incompatible glibcs will also cause an error.

I don't know if that's true of all OSes but Rust libs do carry around metadata with them so if they declare incompatible versions then the compiler could simply error. And linking together native static libraries compiled for different API versions would seem to be squarely in the realm of "you must know what you're doing" (and that's true more broadly when linking native libs). However, considering the current narrow scope of this RFC, it would not be a situation that arises any more often than it currently does. So it may only worth a mention in future possibilities.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually like to update my previous statement, I suspect that it may actually be unsound to use the combination dependency (compiled for newer OS) + user_crate (compiled for older OS) + link (for older OS), since e.g. LLVM may do codegen optimizations that are only valid on newer architectures (which one can do because Apple restricts OS upgrades after a certain point, so we know that newer OS versions only run on newer hardware, and can be required to have for example a certain level of SIMD features).

Related here is #3716.

But I agree that this is only tangentially related to the RFC.

[reference-level-explanation]: #reference-level-explanation

The `os_version_min` predicate allows users to conditionally compile code based on the API version supported by the target platform using `cfg`.
It requires a key and a version string.
The key can be either a `target_os` string or else one of a set of target-defined strings.
Version strings are always target defined (see [Versioning Schema][versioning-schema]) and will be compared against the target's supported version.
For example, `#[cfg(os_version_min("macos", "11.0"))]` has the key `macos` and the minimum version `11.0`, which will match any macOS version greater than or equal to macOS 11 Big Sur.
If a target doesn't support a key, then the `cfg` will always return `false`.

Each target platform will set the minimum API versions it supports for each key.

## The standard library
[the-standard-library]: #the-standard-library

Currently the standard library is pre-compiled meaning that only a single version of each API can be supported, which must be the minimum version.
Third party crates can choose to use a higher API level so long as it's compatible with the baseline API version.
However, there is currently no support for setting a `os_version_min` version above the target's baseline (see [Future Possibilities][future-possibilities]).

## Versioning Schema
[versioning-schema]: #versioning-schema

Version strings can take on nearly any form and while there are some standard formats, such as semantic versioning or release dates, projects/platforms can change schemas or provide aliases for some or all of their releases.
Because of this diversity in version strings, each target will be responsible for validating the version, and defining comparisons on it.

## Linting
[linting]: #linting

By default `os_version_min` will be linted by `check_cfg` in a similar way to `target_os`.
That is, all valid values for `target_os` will be accepted as valid keys for `os_version_min` on all platforms.
The list of additional keys supported by the target will be consulted, which will then be allowed on a per-target basis.
Comment on lines +137 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remark: I want to note here that I want version validation, and I want it everywhere ;).

E.g. writing just #[cfg(os_version_min("macos", "invalid"))] should give an error stating that invalid is not a valid version string, and ideally regardless of what target I am currently compiling for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might work for built-in targets but it can't work for external targets when we don't have the target spec (which admittedly are still unstable but still).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm doubtful of the benefit of this feature to external targets, but yeah, I'd be fine with not having validation for those.


## Future Compatibility
[future-compatibility]: #future-compatibility

The functions for parsing and comparing version strings may need to be updated whenever a new API is added, when the version format changes, or when new aliases need to be added.

# Drawbacks
[drawbacks]: #drawbacks

Each supported platform will need to implement version string parsing logic (or re-use some provided defaults), maintain the logic in response to future changes, and update any version alias tables.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

The overall mechanism proposed here builds on other well established primitives in Rust such as `cfg`.
A mechanism which tries to bridge cross-platform differences under one `min_target_api_version` predicate [was suggested](https://github.com/rust-lang/rfcs/blob/b0f94000a3ddbd159013e100e48cd887ba2a0b54/text/0000-min-target-api-version.md) but was rejected due to different platforms having divergent needs.

For many platforms, the `target_os` name and the `os_version_min` name will be identical.
Even platforms that have multiple possible `versions` relevant to the OS will still have one primary version.
E.g. for `linux` the primary version would refer to the kernel with `libc` being a secondary OS library version.
Therefore it would make sense for the primary target OS version to be a property of `target_os`.
E.g.: `cfg(target_os("macos", min_version = "..."))`.
This means we'd need a more general syntax for `libc` and potentially other versioned libraries where the target OS is ambiguous.

# Prior art
[prior-art]: #prior-art

In C it's common to be able to target different versions based on a preprocessor macro.
For example, on Windows `WINVER` can be used:

```c
// If the minimum version is at least Windows 10
#if (WINVER >= _WIN32_WINNT_WIN10)
// ...
#endif
```

This RFC is a continuation of [RFC #3379](https://github.com/rust-lang/rfcs/pull/3379) more narrowly scoped to just `os_version_min`.
That RFC was in turn an updated version of [this RFC draft](https://github.com/rust-lang/rfcs/pull/3036), with the changes reflecting conversations from the draft review process and [further Zulip discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/CFG.20OS.20Redux.20.28migrated.29/near/294738760).

# Unresolved questions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another unresolved question to add: How should this work in Cargo [target.'cfg(os_version_min(...))'.dependencies] sections?

[unresolved-questions]: #unresolved-questions

Custom targets usually specify their configurations in JSON files.
It is unclear how the target maintainers would add version comparison information to these files.

What exactly should the syntax be?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To further the bikeshedding: os_version_min is definitely wrong, it should at the very least be version_min (since it works for libc too, which is not an OS).

But how about the name available?

exftern "C" {
    #[cfg(any(
        available("libc", "x.y.z"),
        available("macos", "10.12"),
    ))]
    fn foo();
}

if cfg!(available("windows", "10.0.10240")) {
    // ...
}

A nice thing about available is that it reads a tiny bit more like English:

#[cfg(version_min("macos", "10.12"))] // configured where version minimum macOS 10.12
#[cfg(available("macos", "10.12"))]   // configured where available macOS 10.12.

While still avoiding the "does cfg!(xyz("macos", "10.12")) mean >10.12 or >=10.12" issue.

This could also tie in nicely with a macro available! that first does the static check, and then falls back to a runtime version check against e.g. gnu_get_libc_version() (not proposing this macro here, and probably not implement-able everywhere either, but just to get the idea across):

if available!("libc", "x.y.z") || available!("macos", "10.12") {
    // ... Dynamically use some new feature or API, maybe with `dlsym` or libloading.
}

(Note: I'm heavily biased by Swift's @available).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cfg(available) is confusable with cfg(accessible).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted parity with cfg(version), we could do something like cfg(target_version("macos", "10.12")). They both follow the >= rule, so it would be good to align their naming scheme.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be truly consistent we'd need cfg(version) to be like cfg(version("rust", "1.123")).

But ok, I'll change this (again) to use cfg(target_version). However, if there's more bikeshedding I'll probably wait for lang to decide because it's a pain to update.

Copy link
Contributor

@madsmtm madsmtm Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to leave the name as os_version_min for now, and only change it once we reach a consensus. Just noting the alternatives in the RFC text would be enough.

Also, adding another option: cfg(platform_version("macos", "10.12")).

Copy link
Member Author

@ChrisDenton ChrisDenton Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll leave it pending t-lang feedback but add some of the ideas to the RFC.

Though I do think platform_version, os_version and target_version all sound more or less like synonyms to me.

Should we draw a distinction between cases where the `os_version_min` directly implies a specific `target_os` and cases where it doesn't (see alternatives)?

# Future possibilities
[future-possibilities]: #future-possibilities

* The compiler could allow setting a higher minimum OS version than the target's default.
* With the `build-std` feature, each target could optionally support lowering the API version below the default.