loading...

Cargo [features] explained with examples

rimutaka profile image Max ・7 min read

If you feel confused about all the intricacies of Cargo.toml [features] section you are not alone (1,2,3).

First of all, I assume that you have already read the docs at https://doc.rust-lang.org/cargo/reference/features.html to get some foundation of what "features" are before going through these examples. I did read the docs first too, but they left me confused until days later when I had to go much deeper into the topic to make some crates work together. What I think was missing from the docs for me was more examples with explanations how they work. This post was written to fill that gap.

What you will learn from this post

  • What those cryptic feature definitions in Cargo.toml actually mean.
  • How to find the full list of features for a dependency even if they are not listed in the docs.
  • How default_features and optional attributes work.
  • How features work across multiple levels of dependencies.

How to find all features of a package

Crate docs can be quite incomplete. Even if its features are mentioned it's not always clear what they are for and how to use them. There is an easy way of finding out what features available and what may be behind them - check Cargo.toml file of that package.

Consider this description of features in serde_dynamodb crate:

featrues-docs-example.png

If you check the source you'll notice that it fails to mention rustls feature altogether.

[dependencies]
rusoto_dynamodb = { version = "0.43.0", default-features = false, optional = true }

[features]
default = ["rusoto_dynamodb", "rusoto_dynamodb/default"]
rustls = ["rusoto_dynamodb", "rusoto_dynamodb/rustls"]

From the above Cargo.toml file example you can see that there are 2 features:

  • default, which is a special Cargo keyword for when you do not specify any features as in serde_dynamodb = "0.5"
  • rustls, which is only activated if specified as in serde_dynamodb = { version="0.5", features=["rustls"]}

So if we specify nothing (as in serde_dynamodb = "0.5"), the compiler will transform

rusoto_dynamodb = { version = "0.43.0", default-features = false, optional = true }
into
rusoto_dynamodb = { version = "0.43.0", default-features = true }

If we specify serde_dynamodb = { version="0.5", features=["rustls"]} the compiler will transform

rusoto_dynamodb = { version = "0.43.0", default-features = false, optional = true }
into
rusoto_dynamodb = { version = "0.43.0", default-features = false, features=["rustls"] }

Both feature definition in serde_dynamodb include a special notation to set features in a dependency:

  • "rusoto_dynamodb/default" means set default-features = true for dependency rusoto_dynamodb
  • "rusoto_dynamodb/rustls" means apply feature rustls to dependency rusoto_dynamodb

features-example-1

default_features = false

default_features attribute enables or disables any defaults provided by the dependency.

For example, some_core package uses OpenSSL by default with RustLS implementation as an optional feature. OpenSSL and RustLS are mutually exclusive, so to enable RustLS we have to disable the defaults. Your Cargo.toml would have a line like this:

some_core = { version = "0.44", default_features = false, features=["rustls"] }

If we don't use default_features = false then the behavior of some_core is undetermined and will most likely fail at compile time. This will be discussed in more detail later.

The authors of some_core package could use conditional compilation to save us from having to remember to use default_features = false:

#[cfg(feature="rustls")]
fn use_rustls_here {
  ...
}

#[cfg(not(feature="rustls"))]
fn use_openssl_here {
  ...
}

optional = true

optional = true attribute tells the compiler to include the dependency only if it is explicitly mentioned in one of the activated features. In the following example, log is always included, while dynomite-derive will only be included if your Cargo.toml requests derive feature as in dynomite = {version = "*", features = ["derive"]}.

[dependencies]
log = "0.4"
dynomite-derive = { version = "*", path = "../dynomite-derive", optional = true }

[features]
derive = ["dynomite-derive"]

More examples of Cargo [features]

Initially, features have to be declared in Cargo.toml of the package they are used in, that is, in your dependency. Then they are "referenced" in your Cargo.toml file to tell the dependency what you want from it.

Here is an abridged example of feature declarations from Dynomite, a DynamoDB library.

[dependencies]
log = "0.4"
dynomite-derive = { version = "0.8.2", path = "../dynomite-derive", optional = true }
rusoto_core_default = { package = "rusoto_core", version = "0.44", optional = true }
rusoto_core_rustls = { package = "rusoto_core", version = "0.44", default_features = false, features=["rustls"], optional = true }
rusoto_dynamodb_default = { package = "rusoto_dynamodb", version = "0.44", optional = true }
rusoto_dynamodb_rustls = { package = "rusoto_dynamodb", version = "0.44", default_features = false, features=["rustls"], optional = true }
uuid = { version = "0.8", features = ["v4"], optional = true }
chrono = { version = "0.4", optional = true }

[features]
default = ["uuid", "chrono", "rusoto_core_default", "rusoto_dynamodb_default"]
rustls = ["uuid", "chrono", "derive", "rusoto_core_rustls", "rusoto_dynamodb_rustls"]
derive = ["dynomite-derive"]
magic = []

That gives us default and 3 optional features, rustls, derive and magic. You can use these features to tell Dynomite what you need from it in your project.

  • default - activates if no feature is specified or even if another feature is specified without default_features = false. In the example above it lists the name of dependencies that should be included if this feature is activated.
  • rustls - activates if your Cargo.toml has something like dynomite = {version = "0.8.2", default-features = false, features = ["rustls"]}. Its declaration includes a list of dependencies and a name of another feature - derive. So if you specify features = ["rustls"] it is the same as specifying features = ["rustls", "derive"].
  • derive - this feature references a single dependency dynomite-derive. No other conditional dependencies would be included if only derive was requested in your Cargo.toml.
  • magic - this feature does not reference anything. It can only be used for conditional compilation with #[cfg(feature="magic")]

explained with colour

If your Cargo.toml had dynomite = {version = "0.8.2"} it would tell the compiler to use all unconditional dependencies (log) and those listed in default feature declaration:

log = "0.4"
rusoto_core_default = { package = "rusoto_core", version = "0.44", optional = true }
rusoto_dynamodb_default = { package = "rusoto_dynamodb", version = "0.44", optional = true }
uuid = { version = "0.8", features = ["v4"], optional = true }
chrono = { version = "0.4", optional = true }

If your Cargo.toml had dynomite = {version = "0.8.2", default-features = false, features = ["derive"]} it would tell the compiler you only want derive and none of the defaults:

log = "0.4"
dynomite-derive = { version = "0.8.2", path = "../dynomite-derive", optional = true }

If you didn't specify default-features = false all the default features would still be included.

If your Cargo.toml had: dynomite = {version = "0.8.2", default-features = false, features = ["rustls"]} it would tell the compiler you want everything from rustls feature, which also includes derive feature:

log = "0.4"
dynomite-derive = { version = "0.8.2", path = "../dynomite-derive", optional = true }
rusoto_core_rustls = { package = "rusoto_core", version = "0.44", default_features = false, features=["rustls"], optional = true }
rusoto_dynamodb_rustls = { package = "rusoto_dynamodb", version = "0.44", default_features = false, features=["rustls"], optional = true }
uuid = { version = "0.8", features = ["v4"], optional = true }
chrono = { version = "0.4", optional = true }

Dependency of a dependency: beware that features are "additive"

Cargo takes the union of all features enabled for a crate throughout the dependency graph. If multiple crates enable mutually exclusive features of another crate, then all those features will be enabled at build time. The result of that would depend on the implementation of the crate and may produce a compiler error if mutually exclusive crates or features are enabled.

An example of this type of dependency would be Crate X that depends on Crates A and Crate B, while both A and B depend on Crate awesome.

       Crate X
      /        \
Crate A        Crate B
      \        /
    Crate awesome

In the following example both go-faster and go-slower features will be enabled in crate awesome. It will be up to that crate to decide which of the two features prevails.

  • Crate awesome:
[features]
"go-faster" = []
"go-slower" = []
  • Crate A: awesome = { version = "1.0", features = ["go-faster"] }
  • Crate B: awesome = { version = "1.0", features = ["go-slower"] }

Consider a more complicated example with three possible configurations for some_core dependency.

  • Crate awesome:
[dependencies]
some_core_default = { package = "some_core", version = "0.1" }
some_core_openssl = { package = "some_core", version = "0.1", default_features = false, features=["openssl"], optional = true }
some_core_rustls = { package = "some_core", version = "0.1", default_features = false, features=["rustls"], optional = true }

[features]
default = ["some_core_default"]
openssl = ["some_core_openssl"]
rustls = ["some_core_rustls"]

The following combination will make crate awesome depend on some_core_rustls because the resulting tree includes default-features = false, features = ["rustls"] which overrides the default:

  • Crate A: awesome = { version = "1.0" }
  • Crate B: awesome = { version = "1.0", default-features = false, features = ["rustls"] }`

Removing default-features = false results in a compilation error because the same some_core dependency is included twice. Once via default and once via rustls:

  • Crate A: awesome = { version = "1.0" }
  • Crate B: awesome = { version = "1.0", features = ["rustls"] }

The following combination will also result in the same compilation error because package some_core is included twice via some_core_openssl and some_core_rustls:

  • Crate A: awesome = { version = "1.0", default-features = false, features = ["openssl"] }
  • Crate B: awesome = { version = "1.0", default-features = false, features = ["rustls"] }

Useful links

Posted on Jun 25 by:

rimutaka profile

Max

@rimutaka

Solution architecture, microservices and Rust. Available for contract work.

Discussion

markdown guide