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.