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:
Simple Condition:
#[cfg(target_os = "linux")] fn only_on_linux() { println!("This runs only on Linux!"); }
Multiple Conditions:
Using logical operators like
any
,all
, andnot
:#[cfg(any(target_os = "linux", target_os = "windows"))] fn supported_platforms() { println!("This runs on Linux or Windows."); }
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.
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 ()
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.