Mutually exclusive feature flags?


#1

I have a use-case I’ve described here: https://github.com/rust-lang/cargo/issues/5653#issuecomment-430489303

The short version is that I’d like to see mutually exclusive feature flags to ensure that multiple versions of the same crate (via the cargo feature linked above) don’t conflict at link time. This can happen when dealing with ffi crates that link to an external library.

So I guess I was wondering, has this ever come up before? Does the idea seem reasonable? Would it be difficult for cargo to support this functionality?

Maybe it could look something like this in Cargo.toml (not sure if valid syntax):

[package]
mutually_exclusive_features = [
    ["x_feature", "y_feature"],
    ["y_feature", "z_feature"],
]

[features]
default = []
x_feature = []
y_feature = []
z_feature = []

That way the only valid features are none, individual features, or “x_feature” && “z_feature”, enforced by cargo.


#2

Not enforced by cargo, but:

#[cfg(all(feature = "a", feature = "b"))]
compile_error!("features `crate/a` and `crate/b` are mutually exclusive");

#3

I do something similar already, but it doesn’t prevent the aforementioned linker error (can be seen in the earlier link) since cargo doesn’t actually know they’re being used exclusively and so ends up linking both.

Also, I have a set of like 8 (and growing) of these exclusive features and so it’d be a pain to generate every single possible combination by hand…


#4

You don’t need every possible combination, all and any are powerfull enough to write conditions like this:

#[cfg(any(
    all(feature = "a", any(feature = "b", feature = "c")),
    all(feature = "b", any(feature = "a", feature = "c")),
    all(feature = "c", any(feature = "a", feature = "b")),
))]
compile_error!(...);

Yes, it’s somewhat unwieldy, but I think that large number of exclusive features could be an indication of not so optimal architecture.


#5

That’s a good point! But unfortunately it’s not the main issue here, the linker conflict is. And that needs cargo’s help.

The number of features is completely unavoidable unfortunately because it correlates to specific versions of the external API I’m linking against which don’t all share the same set of features.


#6

I am in the process of writing an rfc that will solve your problem as well. Just haven’t been able to finish it.


#7

Interesting. We could create a macro (e.g. in static_assertions package) to perform the expansion


#[macro_export]
macro_rules! assert_unique_feature {
    () => {};
    ($first:tt $(,$rest:tt)*) => {
        $(
            #[cfg(all(feature = $first, feature = $rest))]
            compile_error!(concat!("features \"", $first, "\" and \"", $rest, "\" cannot be used together"));
        )*
        assert_unique_feature!($($rest),*);
    }
}

assert_unique_feature!("a", "b", "c");

#8

This one of the shortcomings of the “features” feature (another is that its called the “features” feature…). Because features don’t impact version resolution, and any library in the tree can turn on any feature of any library upstream of them, we have to treat features as monotonically additive. Using them in an incompatible way would be a misuse of the features feature.

Unfortunately, this is often what you want! Sometimes people just do use them that way, the only reason you can’t is that cargo has a check on the particular thing you want to do. What we need is some other kind of features which act as a switch, and probaby which only the final binary can set (to avoid incompatibilities between libraries).

Libraries using these switch-like featuers would need to be API compatible between the switches, or they wouldn’t work. That’s something which is also difficult to confirm (just as the additivity of the current features is difficult to confirm).


#9

Wouldn’t that assert_unique_feature macro only generate adjacent pairs (“a”, “b”) and (“b”, “c”) but not (“a”, “c”)?


#10

No (pseudo expansion nonsense):

assert_unique_feature!("a", "b", "c");

$first = {"a"}; $rest = {"b"}, {"c"};
for $tail in $rest {
    #[cfg(all(feature = $first, feature = $tail))]
    compile_error!(concat!("features \"", $first, "\" and \"", $tail, "\" cannot be used together"));
}
assert_unique_feature!($($rest),*);

#11

What we need is some other kind of features which act as a switch, and probaby which only the final binary can set (to avoid incompatibilities between libraries).

I like this idea. Additive features are nice when possible, but as a library author it isn’t always possible to expose.


#12

Please send me a link once you’ve written it up :slight_smile:


#13

OT, but it’s interesting how Kotlin deals with a similar problem.

https://kotlinlang.org/docs/reference/platform-specific-declarations.html

Basically, instead of conditional compilation, they have expect keyword for describing “interfaces”, not unlike .h/.mli files.

This is super-useful for IDE, because it can analyze code 100% correctly without knowing “active configuration”.