Skip to Content
Course content

127: Feature Flags in Cargo

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once spent two hours debugging a "missing function" error in a project I had written myself. I knew the function was there—I could see it on my screen—but the compiler insisted it didn't exist. It turns out I had wrapped the function in a feature flag and forgotten to enable that flag in my binary's configuration. It's a rite of passage in Rust: the feeling that the compiler is gaslighting you.

When your functions suddenly vanish

Let's look at a scenario you'll likely run into. Imagine you're building a small library called telemetry. You want to give users the option to export data as JSON, but since serde_json is a heavy dependency, you don't want to force every user to download it.

// In telemetry/src/lib.rs
#[cfg(feature = "json")]
pub fn export_json(data: &str) -> String {
    format!("{{\"data\": \"{}\"}}", data)
}

pub fn log_simple(data: &str) {
    println!("Log: {}", data);
}

Everything looks great in the library. But then you try to use it in your main application:

// In app/src/main.rs
use telemetry;

fn main() {
    telemetry::log_simple("System Start");
    let json = telemetry::export_json("System Start"); // ERROR: no function `export_json` found in module `telemetry`
    println!("{}", json);
}

If you're looking at the telemetry source code, you'll be confused. The function is right there! The problem is that #[cfg(feature = "json")] tells the compiler: "Only include this code if the json feature is active." By default, features are off unless you explicitly mark them as default. To the compiler, that function literally doesn't exist during the build process of your app.

Wiring the toggle in Cargo.toml

To fix this, you need to tell Cargo that your library has a feature called "json", and then tell your application to use it. First, in the library's Cargo.toml, you define the feature:

[features]
# This defines a feature called "json". 
# It's empty for now because we're just using it as a toggle.
json = []

[dependencies]
# We'll get to the dependency part in a second

Now, in your application's Cargo.toml, you don't just list the library; you specify which features you want:

[dependencies]
telemetry = { version = "0.1", features = ["json"] }

Once you save this, the compiler sees the json flag is active, the #[cfg]` attribute evaluates to true, and the function is finally "baked into" the binary. I usually find it helpful to remember that features are additive. Adding a feature should never break existing functionality; it should only add new capabilities.

Tying features to optional dependencies

In the real world, you aren't just toggling a boolean; you're usually toggling a dependency. If your export_json function actually used the serde_json crate, you wouldn't want serde_json to be compiled if the user doesn't need JSON support.

The trick here is the optional = true key. Here is how I set up my Cargo.toml when I want a dependency to be tied to a feature:

[dependencies]
# Making it optional automatically creates a feature with the same name
serde_json = { version = "1.0", optional = true }

[features]
# This tells Cargo: "If the 'json' feature is enabled, also enable the 'serde_json' dependency"
json = ["dep:serde_json"]

Note the dep:serde_json syntax. In older versions of Rust, making a dependency optional automatically created a feature of the same name. Nowadays, using dep: allows you to name your feature (e.g., "json") differently than the crate name (e.g., "serde_json"), which keeps your API cleaner for the end user.

Now, when you wrap your code in #[cfg(feature = "json")], you are guaranteed that serde_json is available in the namespace. If you try to use serde_json outside of that cfg block, the compiler will throw an error—which is exactly what we want. It prevents you from accidentally leaking a heavy dependency into the "core" part of your library.




📋 Practical Task

Implementing a Conditional Compression Module

You are building a data-processing library. You want to implement a compress_data function, but it requires the flate2 crate. To keep the library lightweight, this should be an optional feature.

Requirements:

  • Modify the Cargo.toml to include flate2 as an optional dependency.
  • Define a feature named compression that enables the flate2 dependency.
  • In src/lib.rs, create a function compress_data(data: &str) -> Vec<u8>.
  • Ensure compress_data is only compiled when the compression feature is enabled.
  • Create a second function process_data(data: &str) that calls compress_data only if the feature is enabled, and otherwise just returns the data as bytes. (Hint: Use #[cfg(feature = "compression")] and #[cfg(not(feature = "compression"))]).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.