Include a file twice with different features set

Is it possible to set a feature (or compile-time variables) using annotations, such that I can do something like:

mod v1 {
    #![set_feature("use_v1")]
    include!("common.rs");
}
mod v2 {
    #![set_feature("use_v2")]
    include("common.rs");
}
// cfg(any()) never matches, but let `rustfmt` treat
// `common.rs` as if it was a module:
#[cfg(any())]
mod common;

I didn't find anything such as set_feature in the reference. Is there a way to do it?

No, this isn't possible.

:sob:

I searched for another way to do it, like accessing the current module name. There is std::module_path!, which can be used to do:

main.rs:

mod v1 {
    include!("common.rs");
}
mod v2 {
    include!("common.rs");
}
#[cfg(any())]
mod common;

fn main() {
    v1::foo();
    v2::foo();
}

common.rs:

pub fn foo() {
    if module_path!().ends_with("v1") {
        println!("v1");
    } else if module_path!().ends_with("v2") {
        println!("v2");
    }
}

Not exactly what I wanted though, as using this in sub-sub modules will have to work differently. Also, could I convert module_path!().ends_with(…) into an annotation?

I'm pretty sure that you can't convert it into an annotation unfortunately.

Feature flags are passed from the compiler, so can't be set in code, and can't be different in different parts of a crate. What you are trying to do reminds me of some of the module metaprogramming used in Ocaml. Similar patterns in rust are usually handled on types with traits instead of on modules, but since you don't say what you're trying to do, it's hard to be more specific.

Oh I see it now. In this situation, I would use pub use to re export things from different modules.

I would like to create a Lua binding for different versions of Lua. Since I can't link with multiple (different) versions of Lua anyway (at least I don't know any way to do that, but maybe there is?), this is mostly hypothetical now, as I will be able to set the features outside the source code as compiler options.

So it's not a real problem I have at the time being, unless I also solve the issue to link with two shared (C) libraries which use the same symbol names (e.g. lua_open, lua_newtable, etc).


Most code will be identical for Lua 5.4, Lua 5.3, Lua 5.2, etc., but there will be subtle differences. It would feel easier if I can make a single file for all Lua versions and only use #[cfg(…)] annotations and cfg! in a common.rs file.

I can solve this already if only one version is supported (as I will then use features that are defined in my Cargo.toml). But assuming hypothesizing I would find a way to link multiple Lua libraries with different versions, I wondered if it was possible to set the feature also inside my code for a particular module (instead of Cargo.toml).

Solving this with re-exporting doesn't allow me to make switches within a single line of code in a longer function or method, right?

For example, if I want to do something like:

#[cfg(feature="Lua-5.4")]
type LuaInteger = ffi::lua_Integer;

#[cfg(feature="Lua-5.1")]
type LuaInteger = ffi::lua_Number;

But all the rest of my file would be (nearly) identical (except for a few lines where I could use cfg!, for example).

BTW, Cargo doesn't support mutually-exclusive features either. There's no officially sanctioned way to do this with Cargo features. This is probably the least wrong one:

#[cfg(feature="lua-integer")]
type LuaInteger = ffi::lua_Integer;

#[cfg(not(feature="lua-integer"))]
type LuaInteger = ffi::lua_Number;

but you may need to go as far as publishing two versions of the crate with different semver-major versions (e.g. 51.0.0 and 54.0.0), so that they can coexist without interfering with each other's features.

You might be able to use macros to choose what code to compile:

main.rs :

mod v1 {
    macro_rules! has_feature {
        ("v1", $($code:tt)*) => {$($code)*};
        ($disabled:literal, $($fallback:tt)*) => {};
    }
    include!("common.rs");
}
mod v2 {
    macro_rules! has_feature {
        ("v2", $($code:tt)*) => {$($code)*};
        ($disabled:literal, $($fallback:tt)*) => {};
    }
    include!("common.rs");
}
#[cfg(any())]
mod common;

fn main() {
    v1::foo();
    v2::foo();
}

common.rs :

pub fn foo() {
    has_feature!("v1", println!("v1"));
    has_feature!("v2", println!("v2"));
}

has_feature!{"Lua-5.4",
    type LuaInteger = ffi::lua_Integer;
}
has_feature!{"Lua-5.1",
    type LuaInteger = ffi::lua_Number;
}

Its also possible to define a macro to simplify "setting"/defining features:

macro_rules! set_features {
    ($($features:tt),* $(,)?) => {
        set_features!{@inner 
            dollar = {$},
            features = { $($features,)* },
            ensure_literals = { $($features,)* },
        }
    };
    (@inner 
        dollar = { $dollar:tt },
        features = { $($features:tt,)* },
        ensure_literals = { $($feature_as_literal:literal,)* },
    ) => {
        macro_rules! has_feature {
            $(
                ($features, $dollar($dollar code:tt)*) => {$dollar($dollar code)*};
            )*
            ($dollar disabled:literal, $dollar($dollar fallback:tt)*) => {};
        }
    };
}

which could simplify main.rs :

mod v1 {
    set_features!("v1");
    include!("common.rs");
}
mod v2 {
    set_features!("v2");
    include!("common.rs");
}
#[cfg(any())]
mod common;

fn main() {
    v1::foo();
    v2::foo();
}

(Playground)

2 Likes

Actually, mutually exclusive Cargo features are possible...

#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");

https://doc.rust-lang.org/cargo/reference/features.html#mutually-exclusive-features

It's possible, but it's not a good idea. Cargo cannot detect that two crates have mutually exclusive features, so doing that separates dependencies that rely on the crate into two classes that cannot both be used in the same project. A much better idea is to have two crates, so that if a user wants to depend on both versions, it simply compiles them separately and includes them both, rather than breaking the build.

There's nothing wrong with having two crates. You can pull out common code into a mutually shared dependency, and then users can declare which version they want to use through the crate names rather than feature flags. There's no compile errors anywhere, and you don't have to write fragile macros and includes to get it to work well.

1 Like