The following text is braindump-like description of how I would like to see Rust work with various target options. ( target_arch , target_feature , target_os and others) This approach aims to provide stronger compile time guarantees compared to current cfg setups, remove most of the unsafe in the stdsimd crate and integrate target parameters closer with Cargo.toml . Also it could be useful for improving Rust portability story. While RFC 2045 is an important step towards stabilization of SIMD, I don’t think that the current approach is a good long-term solution. Also take a look at RFC 1868 which introduces somewhat similar approach, but for the smaller problem scope. I’ve seen elements of the described ideas in various discussions, but I will not be able to provide comprehensive overview of previously proposed ideas.

TLDR: this proposal introduces concept of “target restriction context”, which allows us to get stronger compile time guarantees around target parameters, which is especially important for working with SIMD intrinsics. Additionally it improves ergonomics of enabled target features for crates, as cargo will be able to compute the minimally required set of features required for building a given crate.

Description

First lets introduce #[target(..)] attribute:

#![target(os=any("windows", "linux"), feature="sse4")] pub fn foo() { ... } #[target(feature=all("aes", "rdrand"))] pub fn bar() { ... } #[target(os="linux")] pub fn linux_bar() { ... }

Functionally #[target(os=any("windows", "linux"), feature="sse4")] can be perceived as equivalent of #[cfg(all(any(target_os="windows", target_os="linux"), target_feature="sse4"))] . But there is several distinctions:

If crate is compiled with target parameters which do not fit into restrictions specified by #![target(..)] and #[target(..)] it will result in a compilation error with an appropriate error message, e.g. it will be impossible to compile the example above for android. (contrary to #![cfg(..)] ) which will compile an empty crate).

and it will result in a compilation error with an appropriate error message, e.g. it will be impossible to compile the example above for android. (contrary to ) which will compile an empty crate). #[target] restrictions have cascading nature, i.e. bar in the example above has the following effective restriction target(os=any("windows", "linux"), feature=all("sse4", "aes", "rdrand")) and linux_bar has this one target(os="linux", feature="sse4") .

restrictions have cascading nature, i.e. in the example above has the following effective restriction and has this one . More restricted items can not be used in less restricted contexts, e.g. bar and linux_bar can not be used in the foo , even if both restrictions are true for given build parameters.

and can not be used in the , even if both restrictions are true for given build parameters. Items covered by different #[target(...)] can’t have same names, i.e. they stay visible. Thus the following code will result in a compilation error:

#[target(os="linux")] pub fn foo() { ... } #[target(os="windows")] pub fn foo() { ... }

To decide if item can be used in the given restriction context we generally have to use SAT solver. While expressions used in practice should be quite simple and easily solved by existing solvers, in the beginning we could require that logical expression which represents restriction context must follow disjunctive normal form, which allows to trivially solve the problem in linear time.

Runtime and compile time dispatch

Now we need tools for performing compiletime and runtime dispatches depending on target parameters. To do it we introduce the following macros:

// use runtime detection to select which arm to execute let result = runtime_dispatch!( (feature=all("avx2", "sse4.1")) => foo_avx2_sse41(), (feature="sse2") => { // e.g. here we can call SSE2 intrinsics safely let c = _mm_add_epi64(a, b); // code }, _ => foo(), ); // select one of the arms at compile time and inline it into the code let result = compiletime_dispatch!(bar, (os="linux", feature="aes") => bar_linux_aes(), (os="linux") => { // code }, (os="windows") => bar_windows, _ => bar(), );

(names can be bikesheded)

Each macro creates contexts in its match arms with additional restrictions, which allows us to use more restricted items in a safe way. If compiler is able to determine exhaustiveness of match arms, the “default” arm can be omitted:

#![target(os=("macos" || "linux"))] #[target(os="linux")] fn foo_linux() { .. } #[target(os="macos")] fn foo_macos() { .. } pub fn foo() { // we are in the restriction context `os=("macos" || "linux")`, // thus no need for default arm compiletime_dispatch!( (os="linux") => foo_linux(), (os="macos") => foo_macos(), ); } // this function will result in a compilation error pub fn foo2() { foo_linux() }

Escape hatch

If for some reason user will want to circumvent target restrictions he could use an unsafe block:

// here instead of a compilation error we'll get a warning, // which can be disabled by `#[allow(..)]` pub fn foo2() { unsafe { foo_linux() } }

One of the reasons for this escape hatch is to be backwards compatible with SIMD intrinsics which are expected to be stabilized in unsafe variant relatively soon.

Relation with #[target_feature(enable = “…”)]

When we build a crate we fix almost all #[target(..)] parameters, the most notable exception is feature . At this stage for each restriction context we can calculate features which should be enabled. In other words we remove the necessity for #[target_feature(enable = "...")] . As features will be enabled for whole restriction context it will allow compiler to use auto-vectorisation more effectively. For example:

// compiler will be able to use AVX2 for the whole function body #[target(feature="avx2")] fn avx_foo() { .. } // no AVX2 instructions for this function fn plain_foo() { .. }

But note that for some cases ( e.g. ARM in thumb mode) we still may need #[target_feature(disable = "...")] to locally disable target feature even if it’s globally enabled.

Integration with Cargo.toml

Crate level #![target(..)] declaration can be moved to the Cargo.toml :

[package] target = {os=["windows", "linux"], feature="sse4"}

It’s probably can be integrated with [target.'cfg(...)'.dependencies] somehow, but I don’t have good ideas right now.

As cargo will be able to calculate the minimal set of features which should be enabled for given target, it will be able to do it automatically, without any dances around RUSTFLAGS .

Backward compatibility

Items and crates without any applied target restrictions can be used in all restriction contexts. (although it could be useful to see lack of #![no_std] as a restriction context to disallow std dependent crates to be used as dependencies for no_std crate) This feature can be introduced as a lint in 2018 edition and turned to compile time errors in the next edition. unsafe escape hatch will allow to be backwards compatible with stabilization of unsafe intrinsics.

Unresolved questions