Cargo features
A feature is a name a package declares and a build turns on. The code behind it is compiled only when the name is on, so one package can hold an optional dependency and a faster path without forcing either on every user.
This lesson declares two plain features, puts an optional dependency behind a third, and builds the same package four ways: default, no default, one extra feature, and a mistake where the code calls an item that the build configured out.
A feature flag
The [features] table lists the names and what each one turns on. A name with an empty list is a flag, and the default entry names the features a plain build gets.
The program asks cfg! which flags are on and prints a greeting plus one extra line when loud is set. The three builds below differ only in the flags.
[features]
default = ["polite"]
polite = []
loud = []
fn main() {
let message = if cfg!(feature = "polite") {
"Good morning"
} else {
"Hi"
};
println!("{message}");
if cfg!(feature = "loud") {
println!("!!!");
}
}
cargo run
cargo run --no-default-features
cargo run --features loud
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.77s
Running `target/debug/greet`
Good morning
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/greet`
Hi
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
Running `target/debug/greet`
Good morning
!!!
The second build compiled the package again because the flags are part of the fingerprint, and the third reused the first build and only ran it, because the flags it needed were already compiled. That reuse is the reason a build with several flag combinations takes longer the first time and then costs nothing.
An optional dependency
A dependency with optional = true is not compiled unless a feature asks for it, and the feature names it with dep: in front so the dependency does not become a feature name of its own. The source then has two versions of the function, chosen by #[cfg(feature = ...)] and #[cfg(not(...))].
[features]
default = ["polite"]
polite = []
fast = ["dep:itoa"]
[dependencies]
itoa = { version = "1.0.19", optional = true }
#[cfg(feature = "fast")]
fn format(value: i32) -> String {
let mut buffer = itoa::Buffer::new();
buffer.format(value).to_string()
}
#[cfg(not(feature = "fast"))]
fn format(value: i32) -> String {
value.to_string()
}
fn main() {
println!("{}", format(1234));
}
cargo run
cargo run --features fast
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/greet`
1234
Compiling itoa v1.0.19
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.12s
Running `target/debug/greet`
1234
The first build never downloaded itoa, and the second compiled it. Both print the same number, which is the point of a feature that changes the implementation and not the result. A library writes this pattern for a faster algorithm, an optional serialisation format or a binding to a system library. The previous lesson sets up the dependency itself.
Default features
--no-default-features drops the whole default list and keeps only the flags named after it, so a minimal build is one option away. A package that other packages depend on uses the same mechanism: the dependent turns a feature on and the dependency compiles it.
The greeting program without the default flag prints the short message. The command also shows that cargo keeps separate copies of the compiled package for each flag set.
cargo run --no-default-features --features loud
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/greet`
Hi
!!!
The output has the short greeting and the loud line, which are exactly the two flags that were on. A feature that is on by default cannot be turned off by a dependent package except with default-features = false on the dependency line, and that line is the way a minimal build is expressed from above.
Using a feature that is off
The code behind a feature is not compiled when the flag is off, so a call to it is a missing name. The compiler reports E0425 and adds a note that the item exists but was configured out, which is more useful than a plain not-found message.
The program calls fast_format, which is defined only under the fast feature, and the build turns that feature off.
#[cfg(feature = "fast")]
fn fast_format(value: i32) -> String {
value.to_string()
}
fn main() {
println!("{}", fast_format(1));
}
cargo build
Compiling greet v0.1.0 (/tmp/rw7/L081/greet)
error[E0425]: cannot find function `fast_format` in this scope
--> src/main.rs:7:20
|
7 | println!("{}", fast_format(1));
| ^^^^^^^^^^^ not found in this scope
|
note: found an item that was configured out
--> src/main.rs:2:4
|
1 | #[cfg(feature = "fast")]
| ---------------- the item is gated behind the `fast` feature
error: could not compile `greet` (bin "greet") due to 1 previous error
The note names the feature, which is the fix: build with --features fast, or move the call behind the same attribute. The next lesson gives the same flags to several packages at once.
Exercises
- Add a serde-free feature that switches a program from a Vec to a HashMap and build both ways.
- Make a dependency optional, forget the dep: prefix and read the warning cargo prints.
- Build a package with --all-features and check which lines of the program run.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Where is a feature declared?
Answer
In the features table of Cargo.toml
The table names the features and what each one turns on.
-
What does
--no-default-featuresdo?Answer
Drops the default list and keeps only the named flags
The default entry is a feature list, and this option leaves it out.
-
What does
dep:itoain a feature mean?Answer
It turns the optional dependency on without adding a feature of the same name
The prefix says the name refers to the dependency, not to a public feature.
-
What happens to code behind a feature that is off?
Answer
It is not compiled at all
The attribute removes the item before the rest of the compiler sees it.
-
What does this program print when it is built without any feature?
fn main() { let message = if cfg!(feature = "polite") { "Good morning" } else { "Hi" }; println!("{message}"); }Answer
Hi
The cfg macro evaluates to false, so the else branch is the one that runs.
-
What does this program print when it is built without any feature?
fn value() -> i32 { #[cfg(feature = "fast")] { return 2; } #[cfg(not(feature = "fast"))] { return 1; } } fn main() { println!("{}", value()); }Answer
1
The second block survives the configuration and returns one.
-
What does this program print when it is built without any feature?
#[cfg(not(feature = "fast"))] const SPEED: i32 = 1; #[cfg(feature = "fast")] const SPEED: i32 = 2; fn main() { println!("{}", SPEED * 21); }Answer
21
Only the constant of the disabled branch is removed, so the first one stays.
-
What does this program print when it is built without any feature?
#[cfg(feature = "loud")] fn suffix() -> &'static str { "!" } #[cfg(not(feature = "loud"))] fn suffix() -> &'static str { "" } fn main() { println!("hello{}", suffix()); }Answer
hello
Only one of the two definitions survives, and it returns the empty string.
-
What is the first line of the compiler output for this program?
#[cfg(feature = "fast")] fn fast_format(value: i32) -> String { value.to_string() } fn main() { println!("{}", fast_format(1)); }Answer
error[E0425]: cannot find function
fast_formatin this scopeThe function was configured out, so the call has no target.
-
What is the first line of the compiler output for this program?
fn main() { let value = 5; #[cfg(feature = "fast")] let doubled = value * 2; println!("{doubled}"); }Answer
error[E0425]: cannot find value
doubledin this scopeThe let statement belongs to the disabled feature, so the name never exists.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1, with itoa 1.0.19 from crates.io, and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev