#[cfg] Conditional Compilation in Rust

by Ugochukwu Chizaram Omumusinachi

.

Updated Thu Jan 16 2025

#[cfg]  Conditional Compilation in Rust

Have you ever built a cross-compatible game or CLI tool or the simple Russian Roulette game, I have as the banner for this article?

Well if you are wondering how I can target each operating system, sit back as I explain the wonders of conditional compilation and how it is done in Rust.

What is Conditional Compilation?

If you have ever built cross-compatible software you would not need this explanation; but for the rest of us. Conditional compilation is simply compiling only parts of code needed based on some given conditions

Why It Matters:

There are a couple of core reasons why conditional compiling is necessary; Here are some of them :

  • Portability across operating systems

  • Flexible feature management.

  • Optimizing binary size and compile times.


What is the #[CFG]

#[cfg] is a special attribute in Rust used for conditional compilation, it allows you to include or exclude parts of your code based on specific conditions.

These conditions are evaluated at compile time, which means your code can adapt to different operating systems, architectures, or feature sets.

For example, if you’re targeting both Windows and Linux platforms, you can use #[cfg] to include code specific to each platform without needing separate codebases.


So how would you write it & How does the compiler even understand it

The #[cfg] attribute checks for predefined or user-defined conditions (usually in your OS or your cargo.toml file or specified as a compile-time variable and determines whether the annotated code should be included in the compiled binary.

Basic Syntax:

#[cfg(condition)]
// Code to include if `condition` is tru

Examples:

  1. Simple Condition:

    
    #[cfg(target_os = "linux")]
    fn only_on_linux() {
        println!("This runs only on Linux!");
    }
    
  2. Multiple Conditions:

    Using logical operators like any, all, and not:

    
    #[cfg(any(target_os = "linux", target_os = "windows"))]
    fn supported_platforms() {
        println!("This runs on Linux or Windows.");
    }
    
    
  3. Custom Conditions:

    Custom conditions can be defined during compilation:

    
    rustc --cfg custom_flag your_file.rs
    
    

    Code:

    
    #[cfg(custom_flag)]
    fn custom_function() {
        println!("This runs with the custom flag!");
    }
    
    

Basic Use Cases

1. Platform-Specific Code

To write platform-specific logic: like you saw in the banner example


#[cfg(target_os = "windows")]
fn platform_specific() {
    println!("This code is compiled for Windows!");
}

#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("This code is compiled for Linux!");
}

2. Conditional Functions, Modules, and Inline Expressions

  • Functions:

    
    #[cfg(target_arch = "x86_64")]
    fn optimized_for_x86_64() {
        println!("Running on x86_64 architecture!");
    }
    
  • Modules:

    
    #[cfg(feature = "experimental")]
    mod experimental {
        pub fn try_this() {
            println!("Experimental feature enabled!");
        }
    }
    
    
  • Inline Expressions:

    
    fn main() {
        #[cfg(debug_assertions)]
        println!("Running in debug mode!");
    }
    
    

The cfg! Macro for Runtime Checks

As stated earlier, since the cfg! is a compile-time feature you would not be able to make runtime decisions based on conditions, that is where the cfg! macro comes in. Using the cfg! macro you can make checks at runtime anywhere in your code on any conditions specified.

How do they compare cfg! && #[cfg]

While #[cfg] works during compile time, the cfg! macro allows you to check conditions at runtime. This can be very helpful where you cannot afford to affect your binary

  • Key Differences:

    Feature

    #[cfg]

    cfg!

    Evaluation Time

    Compile-time

    Runtime

    Scope

    Removes code if false

    Used for runtime checks

    Performance

    Reduces binary size

    Does not alter binary size

Using cfg!

  • Basic Example:

    
    fn main() {
        if cfg!(target_os = "windows") {
            println!("Running on Windows!");
        } else {
            println!("Running on a non-Windows platform.");
        }
    }
    
    
  • Combining with Logical Conditions:

    
    fn main() {
        if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
            println!("Linux on x86_64 detected!");
        }
    }
    
    

Some common conditions you can use it with #[cfg]

Platform-Specific Conditions

You can use it to target specific platforms, making it easy to write OS-dependent code.

  • target_os: Specifies the operating system. Common values:

    • "linux"

    • "windows"

    • "macos"

    • And even more, check the official rustc chain docs

    An example code where I showcase this

    
    #[cfg(target_os = "linux")]
    fn platform_specific() {
        println!("Running on Linux!");
    }
    
    
  • You can also target different architecture for example

Example:


#[cfg(target_arch = "x86_64")]
fn platform_specific() {
    println!("Running on Linux!");
}

Feature Flags:

Yes, you can target specific features defined in your cargo.toml file, this is exactly how crates like Chrono and SeaORM allow you to include only features you would need.

So how do you do that? Well just like the previous,

You would use #[cfg(feature = "feature_name")] above the specific block, you intend to conditionally compile based on presence of the given feature.

Note, that you would have to specify this feature in your cargo.toml file or you would get compile time errors and the rust analyzer won't stop warning you.

Example:

// setting the feature
[features]
serde_support = ["serde_support"]

// checking for the feature
#[cfg(feature = "serde_support")]
fn enable_serde() {
    println!("Serde support is enabled!");
}


Compilation Modes

This is a personal favorite for me, if you are coming from languages like JS or Python where you have to rely on environment variables to determine your environment mode, you would also find this one interesting.

See in Rust, the compiler can know when you are in dev and production, as all production code would be run with the —release flag ; hence with the #[cfg(debug_assertions)] you can assert this environment state.

Enough with the marketing, I bet you are intrigued, let me show you how it is done.


fn main() {
    #[cfg(debug_assertions)]
    println!("This is a debug build!");

    #[cfg(not(debug_assertions))]
    println!("This is a release build!");
}

Notice how it says assertions; because it is a boolean value, you would have to use logical combinators to handle all cases.

Logical Combinators

As you thought, logical combinators are just a few specified combinators to handle all sort of assertions in a cfg block. It is used in other macro definitions too, but more on that later.

There about a few of them, and you can combine conditions them to form Advanced Configurations:

  • all: All conditions must be true.

  • any: At least one condition must be true.

  • not: Negates a condition.

Here is how you would use them in practice.

// change the all to your preferred combinator
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn linux_x86_64() {
    println!("Linux on x86_64 architecture detected!");
}

Advanced Conditional Compilation

Summarizing everything we have just learned, joined with some advanced concepts I would be mentioning in this section.

You can see how to build advanced conditions during compilation. I would work you through a few, but feel free to test combinations of combinators as you will.

  1. Combining Conditions

You could combine a bunch of conditions to form a very specific condition, for example

 
#[cfg(all(any(target_os = "linux", target_os = "macos"), not(debug_assertions)))]
fn advanced_condition() {
println!("Linux or macOS in release mode!");
}

Just do not forget to wrap everything in a parent ()

  1. Custom #[cfg] Attributes

You can specify custom configuration attributes in your cargo.toml file and as a compile flag with cargo. These custom flags can be used for conditional compilation in your code.

Let us look at a code example:

First, you define custom conditions using cargo build or cargo.toml:

  • Pass custom flags during build:

    
    cargo build --cfg custom_flag
    

Then you can include it like any other flag:


#[cfg(custom_flag)]
fn custom_build() {
    println!("Custom build flag is enabled!");
}

Best Practices and Tips

Minimize Conditional Code

Overusing #[cfg] can make code harder to maintain.

  • Prefer abstractions or platform-specific modules instead.

Design feature flags to:

  • Reduce binary size.

  • Control dependencies and optional functionality.

Document Features

Add documentation for all feature flags in your project:

  • Use comments in Cargo.toml.

  • Include explanations in your README.

In summary

Feel free to experiment on the use cases for this feature, examples like: debugging, platform-specific development, feature-specific development custom builds, and many more use cases

Now I bet you have an idea of how to conditionally compile your rust code, well you can stop here and allow this knowledge to leave your mind, or you can put it into a little practice and see how much better you understand it.

Leave your feedback and let me know if there is anything I missed or you would love to discuss on.

If you want to upskill your backend skills in Rust, check out this course

PS: Have fun running this Russian Roulette.

image (1).png

Course image
Become a Rust Backend Engineeer today

All-in-one Rust course for learning backend engineering with Rust. This comprehensive course is designed for Rust developers seeking proficiency in Rust.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week