How about allowing Rust source code to query whether a language feature is implemented or not in the current compiler, and choose different implementations based on the information?
Let me give an example where this feature will be useful. Recently (at Rust 1.20) MauallyDrop is stabilized under the feature name “manually_drop”. Before that, when you don’t want to automatically drop an object, the state-of-the-art way to do is using the nodrop crate.
As a library author, one may want to use ManuallyDrop if the feature named “manually_drop” is implemented in the underlying compiler, and nodrop::NoDrop otherwise. This is for supporting an old version of Rust and using the currently-introduced feature at the same time. In order to do so, I’d like to write something like:
#[cfg(feature = "manually_drop")]
mod nodrop {
pub use std::mem::ManuallyDrop as NoDrop;
}
#[cfg(not(feature = "manually_drop"))]
extern crate nodrop;
... // use nodrop::NoDrop
An alternative could be using library-provided features declared in Cargo.toml. However, in that case, the users of a library should specify which features to use in Cargo.toml, which can be cumbersome and easy to forget. The ability to directly query the underlying compiler will be much more convenient.
Another alternative could be querying about version numbers. However, version numbers itself doesn’t contain any semantic information, and alternative Rust compilers may have different version policies or different priority on implementing features. On the other hand, querying about feature names is more semantic, and can be used for other Rust compilers.
A drawback is the feature names can clash: Both the Rust compiler and Cargo have their own namespace for features, and e.g. “manually_drop” can be interpreted as both the Rust language feature and a library feature declared in Cargo.toml. There should be a way to distinguish the two. I’d prefer “lang/manually_drop” for the language feature, and “manually_drop” for the library feature.
It might be a good idea to limit this to library features and not language features as the latter may introduce new syntax while the former does not.
For library features, I think this is more about paths existing or not and that could be checkable with:
#[cfg(path_exists(std::mem::ManuallyDrop)]
mod nodrop {
pub use std::mem::ManuallyDrop as NoDrop;
}
#[cfg(not(path_exists(std::mem::ManuallyDrop))]
extern crate nodrop;
That however is limited to paths existing or not and does not determine whether a specific type implements a trait or not, which is useful to check sometimes, so lib/manually_drop may be more general here.
In summation, I’d like to see path_exists be the main approach as the more ergonomic and easy to use solution, but lib/manually_drop as a solution where the former is not sufficient. They are certainly compatible with each other.
Personally I prefer solution with minimal Rust version specified in the Cargo.toml, it will allow us to use new language features without bumping crate’s major/minor version on each such change, which is recommended without this ability. This way if we have crate with version v0.8.5 and crate author wants to use new shiny feature in say Rust 1.30, if there is no changes in the public API he will be able to publish v0.8.6 instead of going with v0.9.0. Using index of crate versions with minimal supported Rust versions, compiler will be able to select appropriate crate version under given constrains. (i.e. Rust 1.25 will use v0.8.5 and not v0.8.6) Also it will result in clear error messages when someone will try to compile crate with unsupported compiler version. epoch kinda will do the same, but I find “marketing epochs” somewhat confusing.
Although your proposal is a bit orthogonal to specifying minimal Rust version, but I think it can result in messy codebases, so I am not sure if we should do it.
That works great for lang features, but not so much for library features.
What I would like is to be able to check for a library feature / path, and gate my code under that, and if and when that particular library feature is stabilized, I don’t even have to publish a new version, it just suddenly starts working on new stable versions and the user and I have to do nothing.
Doesn't this assume the feature never changes prior to or during stabilization? I figured that was a non-starter since it basically means assuming stability guarantees for unstable features.
Incidentally, does anyone know why "depending on a minimum rustc version" hasn't already happened? I've never seen anyone raise an objection to it, and everyone seems to agree we need it, and it seems like a really simple feature to design (just some bikeshedding on where to put it in the .toml). Is it just waiting on cargo to get a stable/unstable features system?
Yeah, you’re right; that could cause problems when changes are made (even if you use paths, a type or trait definitions might change during stabilization…) and stable crates would all of a sudden start breaking using the same version.
However, I believe we can salvage the core idea with a slightly altered proposal:
A better idea? Versioning of feature gates
If you version feature gates for library features, and increment a counter every time you make a change to API, then the nightly version has to opt in to a specific version (default is zero), and if changes are made, the nightly version of the crate has to keep up with those changes. The compiler will only ever keep one version around in its source code. Once stabilization of a feature happens, the feature is stabilized with that particular version.
I suspect there is a large amount of features that just go straight to stabilization without any API changes. Those are mostly uncontroversial additions.
I prefer ability to specify minimum required Rust/stdlib version, so that users on older versions just don’t see incompatible upgrades.
Once Rust gets multiple major independent implementations that don’t have complete feature set, LTS versions, etc. more feature detection may be needed. But for now I think Rust is still at a stage where users can be reasonably expected to update to recent stable version (given decent stability guarantees and rustup it’s not hard), so it’s better for everyone to push users to keep up to date than to make libraries bend backwards to support old versions.