Course contentsShow
← Rust
Lesson 1524 of 1,91013. Metaprogramming with MacrosFree lesson

Conditional Compilation with Macros

You will integrate feature flags into macro expansion to produce optimized builds for specific targets.

In this lesson, you will learn how to leverage Rust’s conditional compilation attributes directly within macro definitions to generate specialized code at compile-time based on your target architecture or feature flags.

Beyond Simple Attributes

We have previously used #[cfg(feature = "...")] to conditionally include modules or functions. However, when building low-level firmware, you often need to change the structure or behavior of an entire API surface based on the hardware target. By placing these attributes inside a macro_rules! definition, you can ensure that the macro expands into entirely different instructions depending on the compile-time configuration.

Conditional Expansion

The macro_rules! engine evaluates cfg attributes during the expansion phase. This allows you to generate code that is physically absent from the final binary if it doesn't match the active configuration, preventing unused code bloat and potential linker errors.

Consider a register-write macro that behaves differently depending on whether you are targeting a simulation environment or physical hardware:

macro_rules! write_reg {
    ($addr:expr, $val:expr) => {
        #[cfg(feature = "hw_target")]
        {
            // Direct memory-mapped I/O for production
            unsafe { core::ptr::write_volatile($addr as *mut u32, $val); }
        }

        #[cfg(not(feature = "hw_target"))]
        {
            // Mock logging for development/simulation
            crate::debug::log_write($addr, $val);
        }
    };
}

Why This Matters for Embedded Systems

When working with hardware-specific intrinsics, you cannot allow code meant for an ARM Cortex-M4 to exist in an x86 test build. By embedding #[cfg] attributes within your macros, you create a "compile-time switchboard." The compiler only expands the branch that matches the target.

This technique keeps your call site clean—you simply call write_reg!(ADDR, VAL)—while the macro handles the heavy lifting of ensuring that only the relevant, platform-specific logic is emitted. Because this happens during expansion, the unused branch is discarded before the compiler even performs type checking, making it an essential tool for creating portable Hardware Abstraction Layers (HALs).

Key Takeaway: By nesting conditional attributes inside macro expansions, you create platform-aware syntax that generates optimized, target-specific code while keeping your application-level code clean and portable.