Compile Time Feature Flags in Rust
Toggling feature flags when you compile for zero runtime cost

18 November 2018

‘Feature flags’ is a term used to refer to configuration for your software project used to enable or disable features. They are generally useful if you want your product to have different features in different environments. Maybe you have a feature that isn’t ready for all users yet, so it’s turned off for production, but on in development. Maybe you have a multiple customers, and certain customers have certain features that others do not.

The Rust compiler has built in support for compile time feature flags. When you compile, you specify if the feature is turned on or off, and it affects the compilation.

Recently, I spent a few months working on a bot for the Entelect Challenge AI programming competition. You don’t need to know much about it to understand this post except that I spent a few months writing the best tower defence playing bot that I could! While working on that, I found several points where I had a new idea I wanted to experiment with but I wasn’t sure if it would work. In these cases, I put the experiment behind a feature flag. This let me easily play the bot against itself to get a feeling for if the new feature was making it better or worse.

This article is an overview of how I used those feature flags and how they work. The full source code for my bot is up on GitHub if you want to take a deep look at the code to see how it all comes together.

Defining your features

The first step to add feature flags in Rust is to define what your features are. You do this by adding a [features] section to your Cargo.toml file. The specifics can be found in the Cargo documentation.

My list of feature flags look like this:

[features]
benchmarking = []
single-threaded = []
debug-decisions = []
reduced-time = []
extended-time = []

energy-cutoff = []
discard-poor-performers = []
heuristic-random = ["lazy_static"]
full-monte-carlo-tree = []
static-opening = []
weighted-win-ratio = []

default = ["energy-cutoff", "discard-poor-performers", "static-opening", "weighted-win-ratio"]

The square brackets after each flag is any optional crates that the feature might require. In my list, my heuristic-random feature requires that the lazy_static crate is included as a dependency. If I’m not using the heuristic-random feature, then the lazy_static crate won’t be included when I compile.

Another thing to point out is the list of default features. If you compile without specifying any features, those are the features that will be turned on. In this case, the default set of features are the features that I found added value and I wanted them active in the tournament.

Turning sections of your code on

The first place where I used a feature flag was in benchmarking my code. After I’ve chosen my move, I need to write the move to a file and quit the program. When I’m working on the program, I’d like to know how many iterations of the game I managed to simulate. When the bot is running in the tournament, I can’t see the benchmarking output anyway. There’s a slight performance impact in counting the number of simulations, so it made sense to turn off this logging in the tournament.

I put the code to calculate the benchmarking information and print it out inside a block prefixed with #[cfg(feature = "benchmarking")]. If I compile with

cargo build --features "benchmarking"

then the benchmarking code will run. If I don’t have the benchmarking feature, then the code is stripped out during compilation: no runtime cost at all if it’s turned off!

pub fn choose_move(state: &BitwiseGameState, start_time: PreciseTime, max_time: Duration) -> Command {
    let mut command_scores = CommandScore::init_command_scores(state);

    let command = {
        let best_command_score = simulate_options_to_timeout(&mut command_scores, state, start_time, max_time);
        match best_command_score {
            Some(best) if !best.starts_with_nothing => best.command,
            _ => Command::Nothing
        }
    };

    #[cfg(feature = "benchmarking")]
    {
        let total_iterations: u32 = command_scores.iter().map(|c| c.attempts).sum();
        println!("Iterations: {}", total_iterations);
    }

    command
}

One function, two implementations

Another way that you can use feature flags is to have two versions of a function: one for when the flag is turned on and one for when the flag is turned off. Only the appropriate function will be compiled. From the perspective of any code that uses these functions, there is only one function that they need to worry about.

Something to keep in mind for this approach is that the two functions must have identical signatures. Even if a parameter is only required for one function, you must pass it into both. The second thing to keep in mind is that, since only the one function is compiled at a time, you may need to compile your code with multiple different configurations to test all of your code.

This example has two ways of running. The first is single-threaded and does a normal iteration over a collection. The other uses Rayon to do a multithreaded iteration. The single threaded option gave me a cleaner results in my profiling, but the multi-threaded approach had better performance.

#[cfg(not(feature = "single-threaded"))]
use rayon::prelude::*;

#[cfg(feature = "single-threaded")]
fn simulate_all_options_once(command_scores: &mut[CommandScore], state: &BitwiseGameState) {
    command_scores.iter_mut()
        .for_each(|score| {
            let mut rng = XorShiftRng::from_seed(score.next_seed);
            simulate_to_endstate(score, state, &mut rng);
        });
}

#[cfg(not(feature = "single-threaded"))]
fn simulate_all_options_once(command_scores: &mut[CommandScore], state: &BitwiseGameState) {
    command_scores.par_iter_mut()
        .for_each(|score| {
            let mut rng = XorShiftRng::from_seed(score.next_seed);
            simulate_to_endstate(score, state, &mut rng);
        });
}

Compile time if expressions

All of my previous examples involved blocks that were being turned on and off with a feature flag. You can also use your feature flag as part of an if expression. This has the benefit of compiling all of the code, even the parts you aren’t using.

This example is the piece of code where I told my bot which of a few high level strategies to choose from. Which strategy to use depended on both the set of features I had turned on and how early in the game it was.

let command = if cfg!(feature = "static-opening") && state.round < strategy::static_opening::STATIC_OPENING_LENGTH {
    strategy::static_opening::choose_move(&state)
} else if cfg!(feature = "full-monte-carlo-tree") {
    strategy::monte_carlo_tree::choose_move(&state, start_time, max_time)
} else {
    strategy::monte_carlo::choose_move(&state, start_time, max_time)
};

Compile time constants

One place that I found feature flags particularly helpful was in setting compile time constants for different environments.

For example, I had the available running time as a constant. For spot checks, compiling with a reduced time made it easier to try out new ideas. For profiling, compiling with an extended running time gave my profiler more data. For the normal case I also had a default value.

#[cfg(not(any(feature = "reduced-time", feature = "extended-time")))]
pub const MAX_TIME_MILLIS: i64 = 1950;

#[cfg(feature = "reduced-time")]
pub const MAX_TIME_MILLIS: i64 = 950;

#[cfg(feature = "extended-time")]
pub const MAX_TIME_MILLIS: i64 = 19950;

Where else can I use these?

Generally speaking, these feature flags can be used for conditional compilation based on several different variables. Programmer-specified features are one thing, but you can also do conditional compilation based on the target operating system, or if you’re compiling tests. I previously wrote a piece about how I was using conditional compilation to compile with WebAssembly-specific code, which uses the same techniques.

Compile time feature flags definitely add extra complexity, by having your program do different things depending on how you compiled it. Be sure to use these tricks responsibly.