Rust’s feature flags are a surprisingly powerful way to manage code variants without resorting to preprocessor macros or brittle build scripts. They let you enable or disable entire blocks of code, or even whole crates, based on the build configuration, making your library usable in a wide range of scenarios.
Let’s see this in action. Imagine you’re building a library that can optionally use a fast, but external, XML parsing crate.
// src/lib.rs
#[cfg(feature = "fast-xml")]
use fast_xml::Reader;
#[cfg(not(feature = "fast-xml"))]
use simple_xml_parser::parse_string; // A hypothetical slower, built-in parser
pub fn parse_xml(xml_data: &str) -> Result<(), String> {
#[cfg(feature = "fast-xml")]
{
let mut reader = Reader::from_str(xml_data);
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf) {
Ok(fast_xml::Event::Eof) => break,
Ok(_) => {} // Ignore other events for this example
Err(e) => return Err(format!("fast-xml error: {}", e)),
}
buf.clear();
}
Ok(())
}
#[cfg(not(feature = "fast-xml"))]
{
match parse_string(xml_data) {
Ok(_) => Ok(()),
Err(e) => Err(format!("simple_xml_parser error: {}", e)),
}
}
}
Now, in your Cargo.toml, you declare this feature:
# Cargo.toml
[package]
name = "my_xml_parser"
version = "0.1.0"
edition = "2021"
[dependencies]
# fast-xml is only needed if the 'fast-xml' feature is enabled
fast-xml = { version = "0.15.0", optional = true }
# A hypothetical built-in parser for when fast-xml is not used
# For demonstration, we'll just use a placeholder. In a real scenario,
# this would be your own module or a different dependency.
# simple_xml_parser = { path = "src/simple_xml_parser", optional = false } # Assuming it's part of the crate
[features]
fast-xml = ["dep:fast-xml"] # This line tells Rust that enabling 'fast-xml' feature enables the 'fast-xml' dependency
With this setup, a user can choose which version of your parser to use.
To build with the fast XML parser:
cargo build --features fast-xml
This will compile your library, including the fast_xml dependency and the code guarded by #[cfg(feature = "fast-xml")].
To build without the fast XML parser (using the slower, built-in version):
cargo build
This will exclude the fast_xml dependency and the associated code, relying solely on the #[cfg(not(feature = "fast-xml"))] blocks.
The core mental model is that #[cfg(...)] is a conditional compilation directive. When the condition is true at compile time, the code within the cfg block is included; otherwise, it’s stripped away entirely. feature = "feature_name" is a specific condition that is true if the feature feature_name is enabled for the current crate being compiled.
You can define multiple features, and they can depend on each other or on other features. For example, a full feature could enable both fast-xml and another feature called json-output.
[features]
fast-xml = ["dep:fast-xml"]
json-output = [] # A feature that doesn't depend on any external crates
full = ["fast-xml", "json-output"]
When you specify optional = true for a dependency in Cargo.toml, it means that dependency will only be compiled if it’s explicitly requested, either by being used directly in the code (and thus pulled in by default) or by being activated through a feature flag. The dep:crate_name syntax within a feature definition is the idiomatic way to declare that enabling this feature will also enable the corresponding dependency.
One subtle but powerful aspect is that features can be "transitive." If your crate A has a feature f1 that depends on crate B’s feature f2 (via dep:B), and another crate C depends on A, then when C enables A’s f1 feature, crate B will also be compiled with its f2 feature enabled if C also explicitly enables f2 for B in its own Cargo.toml. This allows for complex dependency graphs where features can propagate.
The next step is often understanding how to use features to conditionally enable or disable entire modules or even provide alternative implementations for traits.