Item 26: Be wary of feature
creep
Rust allows the same codebase to support a variety of different configurations via Cargo's feature mechanism, which is built on top of a lower-level mechanism for conditional compilation. However, the feature mechanism has a few subtleties to be aware of, which this Item explores.
Conditional Compilation
Rust includes support for conditional
compilation, which is controlled by
cfg
(and
cfg_attr
) attributes.
These attributes govern whether the thing—function, line, block, etc.—that they are attached to is
included in the compiled source code or not (which is in contrast to C/C++'s line-based preprocessor). The
conditional inclusion is controlled by configuration options that are either plain names (e.g., test
) or pairs of names
and values (e.g., panic = "abort"
).
Note that the name/value variants of config options are multivalued—it's possible to set more than one value for the same name:
#![allow(unused)] fn main() { // Build with `RUSTFLAGS` set to: // '--cfg myname="a" --cfg myname="b"' #[cfg(myname = "a")] println!("cfg(myname = 'a') is set"); #[cfg(myname = "b")] println!("cfg(myname = 'b') is set"); }
cfg(myname = 'a') is set
cfg(myname = 'b') is set
Other than the feature
values described in this section, the most commonly used config values are those that the toolchain
populates automatically, with values that describe the target environment for the build. These include the OS
(target_os
), CPU
architecture
(target_arch
),
pointer width
(target_pointer_width
),
and endianness
(target_endian
).
This allows for code portability, where features that are specific to some particular target are compiled in only when
building for that target.
The standard target_has_atomic
option also provides an example of the multi-valued nature of config values: both [cfg(target_has_atomic = "32")]
and
[cfg(target_has_atomic = "64")]
will be set for targets that support both 32-bit and 64-bit atomic operations. (For
more information on atomics, see Chapter 2 of Mara Bos's Rust Atomics and
Locks [O'Reilly].)
Features
The Cargo package manager builds on this base cfg
name/value
mechanism to provide the concept of
features: named selective aspects of the
functionality of a crate that can be enabled when building the crate. Cargo ensures that the feature
option is
populated with each of the configured values for each crate that it compiles, and the values are crate-specific.
This is Cargo-specific functionality: to the Rust compiler, feature
is just another configuration option.
At the time of writing, the most reliable way to determine what features are available for a crate is to examine the crate's Cargo.toml manifest file. For example, the following chunk of a manifest file includes six features:
[features]
default = ["featureA"]
featureA = []
featureB = []
# Enabling `featureAB` also enables `featureA` and `featureB`.
featureAB = ["featureA", "featureB"]
schema = []
[dependencies]
rand = { version = "^0.8", optional = true }
hex = "^0.4"
Given that there are only five entries in the [features]
stanza; there are clearly a couple of subtleties to watch out
for.
The first is that the default
line in the [features]
stanza is a special feature name, used
to indicate to cargo
which of the features should be enabled by default. These features can still be disabled by
passing the --no-default-features
flag to the build command, and a consumer of the crate can encode this in their
Cargo.toml file like so:
[dependencies]
somecrate = { version = "^0.3", default-features = false }
However, default
still counts as a feature name, which can be tested in code:
#![allow(unused)] fn main() { #[cfg(feature = "default")] println!("This crate was built with the \"default\" feature enabled."); #[cfg(not(feature = "default"))] println!("This crate was built with the \"default\" feature disabled."); }
The second subtlety of feature definitions is hidden in the [dependencies]
section of the original Cargo.toml
example: the rand
crate is a dependency that is marked as optional = true
, and that effectively makes "rand"
into
the name of a feature.1 If the crate is compiled with --features rand
, then that dependency is activated:
#![allow(unused)] fn main() { #[cfg(feature = "rand")] pub fn pick_a_number() -> u8 { rand::random::<u8>() } #[cfg(not(feature = "rand"))] pub fn pick_a_number() -> u8 { 4 // chosen by fair dice roll. } }
This also means that crate names and feature names share a namespace, even though one is typically global (and usually
governed by crates.io
), and one is local to the crate in question. Consequently, choose feature names
carefully to avoid clashes with the names of any crates that might be relevant as potential dependencies. It is
possible to work around a clash, because Cargo includes a mechanism that allows imported crates to be
renamed (the
package
key), but it's easier not to have to.
So you can determine a crate's features by examining [features]
as well as optional
[dependencies]
in the crate's
Cargo.toml file. To turn on a feature of a dependency, add the features
option to the relevant line in the
[dependencies]
stanza of your own manifest file:
[dependencies]
somecrate = { version = "^0.3", features = ["featureA", "rand" ] }
This line ensures that somecrate
will be built with both the featureA
and the rand
feature enabled. However, that
might not be the only features that are enabled; other features may also be enabled due to a phenomenon known as
feature
unification. This means that a crate will
get built with the union of all of the features that are requested by anything in the build graph. In other words, if
some other dependency in the build graph also relies on somecrate
, but with just featureB
enabled, then the crate
will be built with all of featureA
, featureB
, and rand
enabled, to satisfy everyone.2 The
same consideration applies to default features: if your crate sets default-features = false
for a dependency but some
other place in the build graph leaves the default features enabled, then enabled they will be.
Feature unification means that features should be additive; it's a bad idea to have mutually incompatible features because there's nothing to prevent the incompatible features being simultaneously enabled by different users.
For example, if a crate exposes a struct
and its fields publicly, it's a bad idea to make the fields
feature-dependent:
#![allow(unused)] fn main() { /// A structure whose contents are public, so external users can construct /// instances of it. #[derive(Debug)] pub struct ExposedStruct { pub data: Vec<u8>, /// Additional data that is required only when the `schema` feature /// is enabled. #[cfg(feature = "schema")] pub schema: String, } }
A user of the crate that tries to build an instance of the struct
has a quandary: should they fill in the schema
field or not? One way to try to solve this is to add a corresponding feature in the user's Cargo.toml:
[features]
# The `use-schema` feature here turns on the `schema` feature of `somecrate`.
# (This example uses different feature names for clarity; real code is more
# likely to reuse the feature names across both places.)
use-schema = ["somecrate/schema"]
and to make the struct
construction depend on this feature:
let s = somecrate::ExposedStruct {
data: vec![0x82, 0x01, 0x01],
// Only populate the field if we've requested
// activation of `somecrate/schema`.
#[cfg(feature = "use_schema")]
schema: "[int int]",
};
However, this doesn't cover all eventualities: the code will fail to compile if this code doesn't activate
somecrate/schema
but some other transitive dependency does. The core of the problem is that only the crate that has
the feature can check the feature; there's no way for the user of the crate to determine whether Cargo has turned on
somecrate/schema
or not. As a result, you should avoid feature-gating public fields in structures.
A similar consideration applies to public traits, intended to be used outside the crate they're defined in. Consider a trait that includes a feature gate on one of its methods:
/// Trait for items that support CBOR serialization.
pub trait AsCbor: Sized {
/// Convert the item into CBOR-serialized data.
fn serialize(&self) -> Result<Vec<u8>, Error>;
/// Create an instance of the item from CBOR-serialized data.
fn deserialize(data: &[u8]) -> Result<Self, Error>;
/// Return the schema corresponding to this item.
#[cfg(feature = "schema")]
fn cddl(&self) -> String;
}
External trait implementors again have a quandary: should they implement the cddl(&self)
method or not? The external
code that tries to implement the trait doesn't know—and can't tell—whether to implement the feature-gated
method or not.
So the net is that you should avoid feature-gating methods on public traits. A trait method with a default implementation (Item 13) might be a partial exception to this—but only if it never makes sense for external code to override the default.
Feature unification also means that if your crate has N independent features,3 then all of the 2N possible build combinations can occur in practice. To avoid unpleasant surprises, it's a good idea to ensure that your CI system (Item 32) covers all of these 2N combinations, in all of the available test variants (Item 30).
However, the use of optional features is very helpful in controlling exposure to an expanded dependency graph (Item 25). This is particularly useful in low-level crates that are capable of being used in a no_std
environment
(Item 33)—it's common to have a std
or alloc
feature that turns on functionality that relies on those
libraries.
Things to Remember
- Feature names overlap with dependency names.
- Feature names should be carefully chosen so they don't clash with potential dependency names.
- Features should be additive.
- Avoid feature gates on public
struct
fields or trait methods. - Having lots of independent features potentially leads to a combinatorial explosion of different build configurations.
This default behavior can be disabled by using a "dep:<crate>"
reference elsewhere in
the features
stanza; see the docs for
details.
The cargo tree --edges features
command can help with determining which features are enabled for which crates, and why.
Features can force other features to be enabled; in the original example, the featureAB
feature forces both featureA
and featureB
to be enabled.