Skip to content
academia.sh

Lesson 08 / 12

Dependencies and Feature Flags

That a piece of source excluded by conditional compilation is never seen by the compiler at all, confirmed by repeating the same observation with two separate feature flags to establish the class (nobody checks it).

Published Updated

In this lesson

  • Runs code to show that a body excluded by conditional compilation is never seen by the compiler
  • Repeats the same observation with two separate feature flags to confirm the class (nobody checks it) is not a coincidence
  • Explains that this measurement does not show what happens once the flag is turned on, only proving today's state
Contents

The Low-Level and Interoperability topic closed with four lessons, and in all four the compiler still saw at least something: a few operations inside an unsafe block stayed unchecked, but another operation in the same block was still checked; a foreign function’s body was never seen, but at least its signature entered compilation; the limit a target imposes was read from the source, and two versions — one relying on the standard library, one not — ran side by side on the native target. In all four, the compiler compiled some part of the source, and the guarantee started from there — the checking narrowed, but it never stopped entirely. The Project Lifecycle topic asks a completely different question: is there a piece that sits in the source but that the compiler never sees at all, and if so, does anyone notice even if that piece breaks?

This lesson looks for the answer in conditional compilation. A section of a source file enters compilation or does not, depending on whether a feature flag is on; a section that does not enter passes through no checking at all, beyond its syntax merely having to be valid. The Rust Ecosystem and Advanced Topics course’s shared question — where does the compiler’s guarantee end here — finds its sharpest answer in this lesson: for some bodies, the guarantee never starts at all.

The course’s four answers — the guarantee still holding, being handed to the writer, falling outside the language, or ending up where nobody checks it — each find their counterpart, one by one, in the Project Lifecycle topic’s four lessons: this lesson shows a body that is never checked at all, the next two show which guarantee a test or a count establishes by running, and the last lesson shows that the same source can hold two claims side by side — one never checked, one genuinely checked.

The Body Left Out of Scope

A manifest file normally declares, through feature flags, which parts of a crate are optional; the package manager passes these flags to the compiler with the --cfg option. Since this course does not use a package manager, the flag state is given directly to the compiler, but the mechanism is identical: an item marked with the #[cfg(feature = "name")] attribute enters compilation only while that flag is on. While the flag is off, the compiler never sees that item at all — it neither runs it through type checking nor resolves its names.

Dependencies and feature flags are usually mentioned together because a flag is frequently used to limit the need for a dependency: if a section relies on a particular piece of external functionality that most users do not need, that section is placed behind a flag, and while the flag is off, neither the section nor the dependency it relies on ever enters compilation. This lesson does not use a dependency outside the standard library — dependencies stay limited to the standard library throughout the course — but the mechanism is identical: #[cfg(...)] leaves an item out of compilation regardless of what it depends on.

Feature flags are mostly used to separate a crate’s optional sections: a section that most users do not need is placed behind a flag, and only the party that wants it turns that flag on. A manifest file also states which of these flags come on by default; unless explicitly turned off, default flags enter every build, and the rest are turned on only when the requesting party reaches for them. The payoff of this is that the unused section is never compiled at all — but what this lesson measures is not the payoff, it is this payoff’s cost: the section that stays off lives at a different level of checking than the section that stays on.

The section below carries two bodies marked with two flags. The first sits under the unopened flag and carries two different errors: it calls a function that does not exist and assigns a string directly to an integer. The second sits under the untried flag and, in a similar way, calls a method that does not exist and assigns its result to the wrong type. Both bodies were deliberately built to violate two different kinds of static checking: one violates name resolution (a call that does not exist), the other violates type checking (a wrong assignment). Both were needed to show that they arrive at the same outcome — silently staying out — because choosing only one kind of error would have let the observation apply to only that error class. The third body is not tied to any flag and enters compilation under every condition.

// flag_bodies.rs — two separate flags that always stay off, one body that always stays on
#[cfg(feature = "unopened")]
pub fn never_checked() {
    this_function_was_never_defined();
    let _x: i32 = "text";
}

#[cfg(feature = "untried")]
pub fn another_unchecked(list: &[i32]) -> i32 {
    let total: i32 = list.parallel_sum();
    let _s: &str = total;
    total
}

pub fn running_body() -> i32 {
    41
}

This file carries no fn main at all; it stands as a source section, and the body below pulls it in with a mod declaration. PL1. The claim here is this: when no flag is on, compilation finishes without warnings even though the two faulty bodies inside flag_bodies.rs remain in the source — because neither one ever enters compilation.

// flag_measurement.rs — both flags off; the two excluded bodies are never checked at all
mod flag_bodies;
use flag_bodies::running_body;

fn main() {
    println!("running body result: {}", running_body());
    println!("is the unopened flag on: {}", cfg!(feature = "unopened"));
    println!("is the untried flag on: {}", cfg!(feature = "untried"));
}
running body result: 41
is the unopened flag on: false
is the untried flag on: false

Compilation printed no warning at all, and the program wrote three lines. Both never_checked and another_unchecked remain in the source, each carrying an undefined call and a type error, and neither was compiled at all in this run. Because the compiler never saw them, it could catch neither the call nor the faulty assignment. A runtime flag query inside the code confirms this from within the program: both flags read as off during the run — both off, both bodiless.

This carries an important distinction for not confusing a flag with an ordinary runtime condition. If the same two faulty bodies had been written inside an ordinary runtime branch instead of #[cfg(...)] — say, a condition that is never true — the outcome would not be the same: even if such a branch’s body never runs at runtime, it still passes type checking at compile time, because it remains present in the source tree. If an undefined call were written there, compilation would fail — a branch that is never reached at runtime does not hide an undefined name. #[cfg(...)] does exactly the opposite: it removes the body from the source tree entirely, and with it from both the name-resolution and type-checking stages. PL2. This difference shows that “code that never runs” and “code that is never compiled” are not the same thing; only the latter falls into this lesson’s class, the nobody-checks-it class.

Distinguishing this from a related idea: the Production Go: Testing, Tooling and Performance course measured build tags as a program’s tool-level knowledge — a build tag decided which file compiled for which operating system or architecture, and it was a tool that reported this. What this lesson measures is entirely different: that the excluded body is never checked at all. There, the question was whether a piece of knowledge came from the source, a tool, or a run; here, the question is whether a part of the source ever enters the compiler’s sphere of interest at all. Both ideas use the #[cfg(...)] attribute — in Rust, target selection and feature selection both go through the same mechanism — but they ask different questions: one asks “which file compiles for where,” the other asks “who checks a body that is never compiled.”

The same observation was repeated with a second flag (untried) because a single flag staying off could have been a coincidence — maybe that flag was designed never to be used at all. PL3. Two different flags, carrying two different kinds of error (an undefined call with a wrong type assignment, then an undefined method with a wrong type assignment), reached the same outcome: a closed flag’s body, whatever it contains, never enters the compilation process at all. This is not a special case of a single example; it is conditional compilation’s general behavior.

A project can reduce this risk by delegating it to a process that compiles every flag combination separately: if every combination is compiled at least once, no flag stays off at that moment, and checking momentarily reaches full coverage. This does not remove the measured class from nobody-checks-it — it only defers the moment of checking to the moment the flag is turned on. Every moment such a process is not run, the bodies behind the source tree’s closed flags keep staying unchecked; what this lesson measures is what happens in that process’s absence, not what is guaranteed in its presence.

A Bounding Measurement

The run above showed that a closed flag’s body does not enter compilation. But that does not mean the body is invisible — it sits in the source, it can be read, it can be tracked in the version control system. What is invisible is whether the compiler looks at it. The run below shows this distinction from within a running program: a block set aside for a closed flag’s body is never executed by the program itself, either.

// body_visibility.rs — a closed flag's body is invisible from the run, too
mod flag_bodies;
use flag_bodies::running_body;

fn main() {
    let result = running_body();
    #[cfg(feature = "unopened")]
    {
        println!("this line never compiles");
    }
    #[cfg(not(feature = "unopened"))]
    {
        println!("unopened flag is off: its body stays in the source, never entered the run");
    }
    println!("running body returned {} again", result);
}
unopened flag is off: its body stays in the source, never entered the run
running body returned 41 again

PL4. Both blocks are written inside the same main function, but only one enters the run — which one enters is decided at compile time, by looking at the cfg attribute. Both blocks exist in the source; only one exists in the run. This is not a promise conditional compilation makes to the reader, it is a fact that can be derived from the source itself — but this measurement itself carries a limit.

In this run, #[cfg(...)] sits not on an item (a function) but on an expression block inside main; the attribute itself can apply to functions, to structs, and to a single expression block alike. In a real project this means that only a small part of a function — say, a logging call or an extra check — can be placed behind a flag while most of the function stays shared; tying the entire function to the flag is not required. The measured class does not change: whatever level it is applied at, a block that stays off still never enters compilation at all.

In the run above, #[cfg(feature = "unopened")] and #[cfg(not(feature = "unopened"))] were written at the same spot, as two mutually exclusive blocks. This is another common form flags take: one behavior runs while a flag is on, its opposite runs while the flag is off, and both stay visible in the source. The compiler only compiles whichever of the two matches the flag’s state, and leaves the other out entirely, just like never_checked above — the side that stays off is not faulty here, merely off; but the fact that the compiler never looks at it does not change.

This measurement shows that a closed flag’s body is not compiled today; it does not show whether the body would or would not compile once the flag is turned on. The only way to show that is to actually turn the flag on and try compiling, but never_checked and another_unchecked are deliberately built so that they would not compile once turned on — running a source that does not compile is not possible in this lesson. So the measurement only proves this: today, nobody checks a closed flag’s body.

Comparing this with an ordinary unused item makes the distinction clearer. If a function is written tied to no flag at all but is never called, the compiler notices and prints an unused warning — because that function did enter the compilation process, had its name resolved and its type checked, and was never called. A closed flag’s body never gets this warning, because it never entered the compilation process at all; the reason for the silence is not “known to be unused and ignored,” it is “treated as if it had never existed.” Because every code block delivered in this lesson compiles without warnings, this comparison is not run here, but the difference between the warning the compiler gives unused code and the warning it does (not) give a closed flag’s body explains why the nobody-checks-it class belongs to closed flags specifically.

What happens once it is turned on is not known until it is turned on — and this unknown is the direct reason a closed flag’s body loses sync (rots) with the rest of the source over time. As the source tree grows, as the types and function signatures around it change, a closed flag’s body never has to keep up with any of these changes; because the compiler never checks it, the mismatch only surfaces on the day the flag is turned on.

This limit does not end with a single flag, either. Even if a source tree carries only two independent flags, the on/off pair produces four possible compilation states; as the number of flags grows, the number of possible states grows exponentially. An ordinary build pipeline actually compiles only a few of these states — usually the extremes, where everything is off or everything is on — and most of the combinations in between are never tried. The run above showed one of these extremes by leaving both flags off in a single source file; a state where unopened and untried are turned on together was never built in this lesson, because such a state would bring both bodies into compilation, and both are deliberately built so that neither would compile. As the number of untried combinations grows, predicting when a closed flag will be turned on and what happens at that moment also grows harder. This lesson showed only two flags, and only the most extreme of the four possible combinations (both off); the remaining three combinations — only unopened on, only untried on, both on — stayed outside this measurement’s scope, and that very exclusion is itself another instance of the measured class (nobody-checks-it).

Summary

  • An item marked with #[cfg(feature = "name")] never enters the compiler while that flag is off; compilation finishes without warnings even if the item carries an undefined call and a type error.
  • This observation was repeated with two different flags (unopened, untried) and two different kinds of error; the outcome stayed the same in both.
  • The guarantee’s class is nobody checks it: neither the compiler, nor a tool, nor the running program checks a closed flag’s body.
  • An ordinary runtime branch (like if) only removes the body from the execution path, and the body still passes type checking; #[cfg(...)] removes the body from the source tree, and type checking never even starts — the two forms of exclusion are not the same thing.
  • This measurement’s limit: it shows that a closed flag’s body is not compiled today, but it does not show whether it would compile once turned on — that is known only once the flag is genuinely turned on.
  • A body behind a flag that stays off cannot keep up with changes in the source over time; noticing this is left to the moment the flag is turned on.

Next Step

This lesson showed how a body nobody looks at can sit silently; the reason for that silence was not a hidden error, it was that the body never entered the compilation process at all. The next lesson asks the opposite question: a body enters compilation, passes type checking, and can still run wrong — why is the compiler’s checking not enough to say a body runs correctly, and how much can a test that runs to close this gap be trusted? The Testing and Property-Based Testing lesson answers this question, one the compiler never asks, not with a closed flag this time but with a property tested by running.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close