Skip to content

Rust Attributes, cfg, Features and Editions

Rust configuration can change which items exist before type checking; a Cargo feature is not a runtime switch. This lesson tests a small library with its optional API both absent and present, and separates language editions from compiler versions and build profiles.

Learning goals and prerequisites

After macros, recognise attributes that configure compilation rather than generate code. You will choose between cfg and cfg!, declare additive Cargo features, and test configurations rather than assuming one successful build covers them all.

Use stable Rust and Cargo with edition 2024. This dependency-free program has Linux fixture output on the Pi; it does not identify the physical board or change the OS. No hardware access or compiler installation is needed.

Rust attributes: inner, outer, active and inert

An outer attribute such as #[derive(Debug)] applies to its following item. An inner attribute such as #![forbid(unsafe_code)] applies to the enclosing crate or module. Attributes can affect lint levels, conditional compilation, documentation, representation or macro expansion; they are not all executable annotation functions. See the attribute Reference.

Our crate forbids unsafe code and denies misspelled configuration names. #[cfg] removes the attributed item when its predicate is false. #[cfg_attr] conditionally applies another attribute. Both differ from the procedural attribute implemented in lesson 24. Lint levels express policy: warn reports, deny makes a lint an error, and forbid cannot be overridden by a lower-level allowance. A clean lint run is not proof of correctness.

Create the three-file example

mkdir -p rust-config/src
cd rust-config
# Create the three files below.
cargo fmt --check
cargo check --offline
cargo test --offline
cargo run --offline --quiet
cargo test --offline --no-default-features
cargo test --offline --features detailed
cargo run --offline --quiet --features detailed
cargo test --offline --all-features --release
rustc --print cfg

Cargo.toml

1
2
3
4
5
6
7
8
[package]
name = "rust_config"
version = "0.1.0"
edition = "2024"

[features]
default = []
detailed = []

src/lib.rs

#![forbid(unsafe_code)]
#![deny(unexpected_cfgs)]

pub const UNIT: &str = "mC";
pub static FIXTURE_NAME: &str = "Pi 4B fixture";

#[cfg(target_os = "linux")]
pub const PLATFORM: &str = "linux";

#[cfg(not(target_os = "linux"))]
pub const PLATFORM: &str = "other";

#[cfg_attr(feature = "detailed", derive(Debug))]
pub struct Mode;

pub const fn valid(value: i32) -> bool {
    value >= -100_000 && value <= 150_000
}

#[cfg(feature = "detailed")]
pub fn detail(value: i32) -> String {
    format!("{FIXTURE_NAME}: {value} {UNIT}")
}

pub fn render(value: i32) -> Option<String> {
    if !valid(value) {
        return None;
    }
    #[cfg(feature = "detailed")]
    let rendered = detail(value);
    #[cfg(not(feature = "detailed"))]
    let rendered = format!("{value} {UNIT}");
    Some(rendered)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fixture_policy_includes_both_boundaries_and_zero() {
        const ZERO_VALID: bool = valid(0);
        assert!(ZERO_VALID);
        assert!(valid(-100_000));
        assert!(valid(150_000));
        assert!(!valid(150_001));
        assert_eq!(render(-100_001), None);
    }

    #[test]
    fn common_api_remains_available_in_both_configurations() {
        assert!(render(0).unwrap().ends_with("0 mC"));
        assert!(render(-500).unwrap().ends_with("-500 mC"));
    }

    #[test]
    fn target_predicate_selects_exactly_one_platform() {
        if cfg!(target_os = "linux") {
            assert_eq!(PLATFORM, "linux");
        } else {
            assert_eq!(PLATFORM, "other");
        }
    }

    #[test]
    fn raw_identifier_is_not_a_raw_string() {
        let r#gen = r#"{"reading":0}"#;
        assert_eq!(r#gen.as_bytes()[0], b'{');
        assert!(r#gen.contains("reading"));
    }

    #[cfg(not(feature = "detailed"))]
    #[test]
    fn disabled_detail_keeps_plain_output() {
        assert_eq!(render(0).as_deref(), Some("0 mC"));
    }

    #[cfg(feature = "detailed")]
    #[test]
    fn enabled_detail_adds_api_output_and_debug_derivation() {
        assert_eq!(detail(0), "Pi 4B fixture: 0 mC");
        assert_eq!(format!("{:?}", Mode), "Mode");
        assert_eq!(render(0).as_deref(), Some("Pi 4B fixture: 0 mC"));
    }
}

src/main.rs

1
2
3
4
5
6
7
#![forbid(unsafe_code)]

fn main() {
    println!("platform={}", rust_config::PLATFORM);
    println!("detailed={}", cfg!(feature = "detailed"));
    println!("{}", rust_config::render(46_700).unwrap());
}

Default output on Linux:

1
2
3
platform=linux
detailed=false
46700 mC

Output with --features detailed:

1
2
3
platform=linux
detailed=true
Pi 4B fixture: 46700 mC

cfg removes code; cfg! produces a boolean

The conditional compilation Reference documents predicates including all, any and not, and target keys such as target_os, target_arch and target_pointer_width. Our complementary cfg attributes select one PLATFORM and one rendering binding. The excluded items are not ordinary false branches waiting for runtime execution.

cfg! evaluates its predicate during compilation but expands to a boolean expression. Both branches of an ordinary if must still be valid code. Our platform test is valid either way because PLATFORM always exists. An if cfg!(feature = "detailed") that calls detail cannot type-check when detail is excluded. Use cfg on that code instead. The Reference also documents cfg_select! for selecting syntax; do not treat cfg! itself as item removal.

Target settings describe the compilation target, not necessarily the machine running the compiler during cross-compilation. aarch64 alone does not prove that an application runs on a Pi, and linux alone does not prove that a temperature file exists. Inspect available runtime resources separately in lessons 27–28.

Cargo features are additive build inputs

The Cargo feature documentation describes declared names, defaults and optional dependencies. Cargo sets corresponding feature configuration values for rustc. Our default set is empty; --no-default-features therefore makes no change here. --features detailed and --all-features both enable the single declared feature.

When dependencies request features of the same resolved package, their enabled feature sets can be combined. Do not rely on one caller disabling a feature that another caller enables. Design features to add capability rather than select mutually incompatible modes. Our feature adds a function and Debug support; its deliberately different output is a contract to consider before using such a feature in a machine-readable format. A feature does not install hardware or dynamically reconfigure an already-built binary.

Cargo's configuration checking knows declared features. Our deny(unexpected_cfgs) makes a typo such as feature = "detialed" an error. Declaring a custom cfg as expected is separate from enabling it; avoid suppressing all configuration warnings to hide spelling mistakes.

Editions, compiler versions, MSRV and profiles

An edition selects language compatibility rules for a crate, while rustc --version identifies the toolchain. Updating a compiler does not rewrite edition = "2024", and setting the edition does not install a compiler. Different-edition crates can depend on one another. A feature named detailed has nothing to do with either edition or nightly language gates.

The reserved gen identifier illustrates edition-sensitive syntax: r#gen is a raw identifier, whereas r#"..."# is a raw string with no escape processing. The test also uses a byte character literal b'{'; this does not change its string into arbitrary binary data.

The optional rust-version manifest field declares a supported minimum toolchain version. It is a support promise to verify, not a compiler selector or proof that every feature works on that version. This example omits an untested minimum and records the actual compiler below.

--release selects a Cargo build profile, not an edition. debug_assertions can change between profiles, but essential input validation must not depend solely on debug_assert!. Constants describe values usable in constant contexts; statics name shared storage. Our immutable static fixture label needs no unsafe access. Mutable global storage and edition-2024 unsafe attributes/extern blocks belong to the next lesson.

Deliberately failing: a false branch still needs valid names

In a separate copy, replace only src/main.rs with:

1
2
3
4
5
fn main() {
    if cfg!(feature = "detailed") {
        println!("{}", rust_config::detail(0));
    }
}

Without the feature, cargo check reports E0425: detail is configured out. With the feature this program is valid. A robust repair places #[cfg(feature = "detailed")] on the println statement, so both enabled and disabled builds are valid; only testing the enabled build would miss the defect.

Exercises and troubleshooting

  1. Make that cfg-attribute repair. Test both configurations and predict that only the enabled run prints the detail line.
  2. Misspell detailed in a source cfg. Verify the unexpected_cfgs error rather than adding allow(unexpected_cfgs).
  3. Enable a nonexistent Cargo feature on the command line. Expect Cargo to reject the request before it can serve as a runtime option.
  4. Remove r# from r#gen. Edition 2024 rejects the identifier. Compare an isolated edition-2021 copy on the same toolchain; do not downgrade the entire course or install another compiler.
  5. Run tests with default, no-default, detailed and all-features configurations in debug and release. Count five tests per configuration; the final configuration-specific test changes, rather than both existing at once.
  6. Explain why the common API works without detailed, why the optional API is absent, and what adding a nonempty default feature would change.
  7. Compare rustc --print cfg with the documented keys. Do not infer live hardware capability from the compiler's target metadata.

Verification and next step

On October 10, 2026, this lesson was verified on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. The exact three-file program passed formatting, checks, five tests per configuration in debug/release and exact outputs across default, no-default, detailed and all-features builds. The cfg-attribute repair passed the same matrix. Misspelled source configuration, excluded function access, an unknown Cargo feature and the bare gen identifier failed as expected; the false-branch example compiled with detailed enabled, and the identifier worked in an isolated edition-2021 copy on the same toolchain. Compiler target settings were recorded. These checks do not establish support for every target or an older compiler.

Continue with unsafe Rust, raw pointers and FFI: explain safe-wrapper obligations and complete the Part III worker checkpoint.

Previous: macros · Course overview

Donate