4

I want to conditionally enable run-time checks and logging, independently from each other and from debug and release mode. So I've started adding two features to my project, one called "invariant-checking" and one called "logging". Ultimately i want their use to be through macros I define in a crate which is visible project-wide.

I had assumed that if I filled out the features section the same way in all of the crates the same way then when I activated the feature while compiling the bin crate, then all the lib crates would also have the feature enabled, but this is not the case! How can I enable and disable features across multiple crates? Hopefully this can be done by only changing one thing like the command-line arguments to cargo.

To clarify exactly what I want, here's an example, which I will also reproduce below:

There are three crates, the main, bin, crate, and two lib crates, called "middle" and "common". Here are the relevant parts of the relevant files:

main.rs

extern crate common;
extern crate middle;

fn main() {
    common::check!();

    middle::run();

    println!("done");
}

the main Cargo.toml

[dependencies]

[dependencies.common]
path = "libs/common"

[dependencies.middle]
path = "libs/middle"

[features]
default = []
invariant-checking = []
logging = []

middle's lib.rs

extern crate common;

pub fn run() {
    common::check!();

    common::run();
}

middle's Cargo.toml

[dependencies]

[dependencies.common]
path = "../common"

[features]
default = []
invariant-checking = []
logging = []

common's lib.rs

#[macro_export]
macro_rules! check {
    () => {{
        if cfg!(feature = "invariant-checking") {
            println!("invariant-checking {}:{}", file!(), line!());
        }
        if cfg!(feature = "logging") {
            println!("logging {}:{}", file!(), line!());
        }
    }};
}

pub fn run() {
    check!()
}

and finally common's Cargo.toml

[dependencies]

[features]
default = []
invariant-checking = []
logging = []

When i run cargo run --features "invariant-checking,logging" I get the following output

invariant-checking src\main.rs:5
logging src\main.rs:5
done

but want it to log in middle and common as well. How can I transform this project such that it will do that, and still allow me to get only "done" as output by changing only one place?

Ryan1729
  • 940
  • 7
  • 25
  • 1
    You should define different macros depending on what features are selected in `common`, so feature selection happens at compile time and only in the `common` crate. Then you only need to declare the features in that crate, and switching the features will have global effect, as long as all crates use the same version of`common`. – Sven Marnach Nov 03 '18 at 07:54
  • I believe your question is answered by the answers of [How do I 'pass down' feature flags to subdependencies in Cargo?](https://stackoverflow.com/q/40021555/155423). If you disagree, please [edit] your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Nov 03 '18 at 14:26
  • @SvenMarnach That certainly sounds like less setup and maintenance than passing the flags to every single crate. If I understand correctly you are suggesting defining multiple macro with the same name and selecting which are enabled with attributes like `#[cfg(feature = "logging")]` and `#[cfg(not(feature = "logging"))]`. That works. And at least in my toy example I only need to mention the features in the main and `common`'s `Cargo.toml` files! – Ryan1729 Nov 03 '18 at 18:13
  • @Shepmaster I suppose that previous answer might have technically allowed me to figure out the answer, but I found gnzlbg 's explanation more helpful than just pointing to small paragraph of documentation I missed. Go ahead and mark this question as you please though. – Ryan1729 Nov 03 '18 at 18:18
  • @Ryan1729 Yes, that's what I meant. I can expand in an answer now that I'm at my desk (wrote the comment on mobile). – Sven Marnach Nov 03 '18 at 18:18

2 Answers2

5

How can I enable and disable features across multiple crates?

A Cargo.toml can add features that transitively enable other features which are allowed to belong to dependencies.

For example, in the Cargo.toml of a crate which depends on crates foo and bar:

[dependencies]
foo = "0.1"
bar = "0.1"

[features]
default = []
invariant-checking = [ "foo/invariant-checking", "bar/invariant-checking" ]
logging = [ "foo/logging", "bar/logging" ]

This crate adds the invariant-checking and logging features. Enabling them transitively enables the respective features of the crates foo and bar, so that

cargo build --features=logging,invariant-checking

will enable the logging and invariant-checking features in this crate and also in its dependencies foo and bar as well.

In your particular case, you probably want main to transitively enable the features of middle and common, and for middle to transitively enable the features of common.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
gnzlbg
  • 7,135
  • 5
  • 53
  • 106
1

The macro definitions in their current form have a problem: The code inside the macro gets inlined whenever the macro is used, and then compiled in the context where it got inlined. Since you use runtime feature checks like

if cfg!(feature = "invariant-checking")

this means that you need to define the features in every crate where you are using the macro. In the common crate itself, on the other hand, the feature is never queried and thus redundant.

This seems completely backwards to me. The feature flag should be only queried in the common crate, and using the macro should not require first defining a feature flag in the crate that uses it. For this reason, I suggest using compile-time checks to select what macro to define:

#[cfg(feature = "invariant-checking")]
macro_rules! check_invariant {
    () => ( println!("invariant-checking {}:{}", file!(), line!()); )
}

#[cfg(not(feature = "invariant-checking"))]
macro_rules! check_invariant {
    () => ()
}

#[cfg(feature = "logging")]
macro_rules! logging {
    () => ( println!("logging {}:{}", file!(), line!()); )
}

#[cfg(not(feature = "logging"))]
macro_rules! logging {
    () => ()
}

#[macro_export]
macro_rules! check {
    () => ( check_invariant!(); logging!(); )
}

This way, you will only need to define the feature in the common crate, as it should be. As long as you only use a single version of that crate, switching the flag on and off has global effect.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • I had assumed that `if cfg!(...) { /*...*/ }` would be easy to remove by the compiler. I tried out some simple examples on [godbolt](https://godbolt.org/z/Q2Nnex) and it seems like those are successfully removed. It's possible that more complex cases with lots of `cfg!` calls aren't though, so this technique seems useful since it should work in every case. – Ryan1729 Nov 03 '18 at 18:48
  • @Ryan1729 if the configuration setting is known statically at compile time, the optimizer should _always_ be able to resolve the branch statically. However, the fact that this is a runtime check was only a side note. The more important point is the context in which the feature gets resolved. This version resolves all feature queries in the `common` crate, while your version resolved them in the crate that uses the macro. – Sven Marnach Nov 03 '18 at 20:35