tl;dr - Update your Cargo.toml
and make sure doc_auto_cfg
is turned on in lib.rs
How do you properly show feature specific functionality (and mark them with the required feature) for library crates in 2024?
First, you need to update your Cargo.toml
:
[package.metadata.docs.rs]
all-features = true
If you don’t want all features to be enabled (if you want some features and related code to be legitimately hidden from docs), you can use specific features:
[package.metadata.docs.rs]
features = [ "one", "three" ]
In addition to the above, you have to add the following to your src/lib.rs
:
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
Let’s break down what this does real quick:
cfg_attr
is a feature that let’s you optionally enable an attribute if a cfg flag is set is setdocrs
is an cfg attribute that is set when docs.rs tries to build your documentationdoc_auto_cfg
is a nightly feature (for now) that automatically adds #[doc(cfg(feature = "..."))]
to code that requires a given featureThe combination of these produces docs that expose feature-gated functions/traits/structs/etc and notes which features they are gated by.
Features are a really amazing feature of Rust. It’s not like Rust invented feature gating, feature flagging or conditional compilation, but I’ve rarely seen the feature be so ergonomic in other languages – and I’ve used a bunch of other languages.
In other languages what would you do if you had a feature that you only wanted to use if the user opted into it?
Most of the time this just isn’t done. The most obvious solution is a if !someFeatureEnabled() { ... }
block in all the code that supposedly used that feature, and some language will actually give you a proper feature system and/or it’s terrible cousin, a clunky macro system.
Rust provides some of the most ergonomic support for doing this that I’ve seen. Let’s say your library that prints a message but sometimes you want that message to have emojis, but only if the person who is using your library loves emojis.
First you add the feature to your Cargo.toml
:
[features]
emojify = []
And in your code, you can write a function that uses the feature
fn greet() -> String {
#[cfg(feature = "emojify")]
let s = String::from("Hey there 👋 ")
#[cfg(not(feature = "emojify"))]
let s = String::from("Hello, how are you?")
s
}
Of course in most normal code you might choose to pass through some sort of configuration value to greet()
or centralize the creation of these messages, but for really cross-cutting concerns that show up everywhere and are somewhat “global”, this is a very ergonomic and simple-to-grok way to solve the problem!
When your user pulls in your library in their own Cargo.toml
, they can specify the feature:
your-awesome-lib = { version = "x.x.x", features = [ "emojify" ] }
You can also use cargo add
:
cargo add your-awesome-lib --features=emojify
Of course, you should make sure to update your documentation to let people know the how and why of the feature.
When writing interpreted languages like NodeJS/Python/Ruby, cross-platform support is essentially “guaranteed”. You write high level code and some interpreter does the hard work of calling the appropriate platform-specific syscalls and doing the work for you.
Even languages like Haskell don’t have a super ergonomic solution to this – you essentially end up only being able to put the if
at the module level only (or worse, using CPP style ifdef
s).
Rust also does that (it’s standard library has cross-platform APIs), but it gives you a level of control and makes it pretty easy to change up what you do, per-platform:
fn vibecheck() -> String {
#[cfg(all(not(unix), not(windows)))]
let platform = "???";
#[cfg(unix)]
let platform = "unix";
#[cfg(windows)]
let platform = "windows";
format!("You're on [{platform}], consider Arch Linux.")
}
Isn’t that simple? You can now build functions that are platform aware quite easily, and let other functionality ignore this facet at higher levels of abstraction.
cfg
s!One of the cool things that Rust enables that you may not have thought about is that you can introduce your own cfg
tags.
All you need is some extra code in your library’s build.rs
script.
The simplest example is creating a simple boolean cfg that is either there or it’s not:
fn main() {
// Declare your intent to use a custom cfg (new syntax in Rust 1.80)
println!("cargo::rustc-check-cfg=cfg(it_works)");
// Actually set the cfg value as present
println!("cargo::rustc-cfg=it_works");
}
You can also create cfgs that have a range of values:
fn main() -> {
// Declare your intent to use a custom cfg (new syntax in Rust 1.80)
println!(r#"cargo::rustc-check-cfg=cfg(it_works, values("yep", "nope"))"#);
// Actually set the cfg to a value
//
// In a realistic codebase you'd probably do more to figure out this value below
let it_works = "yep";
println!(r#"cargo::rustc-cfg=it_works="{it_works}""#);
}
In your code, you can now use the config as if it was built into the language and Rust guarantees that it_works
is either "yep"
or "nope"
:
src/lib.rs
#[cfg(it_works = "yep")]
fn do_it() -> &'static str {
"JUST DO IT!"
}
#[cfg(it_works = "nope")]
fn do_it() -> &'static str {
"JUST DON'T IT!"
}
This is a silly example but you can use this for a LOT of different things – you can:
docker
installed?)