Any Such Thing as `#[cfg]` Aliases?

I've been doing some work on gfx and surfman lately which, due to the nature of providing cross-platform APIs with platform specific backends, can have lots of #[cfg] checks. You would wish that these could always be simple and apparent such as #[cfg(windows)] or #[cfg(feature = "x11")], but often times you need extra constraints such as:

#[cfg(all(any(feature = "sm-x11", all(unix, not(any(target_os = "macos", target_os = "android"))))))]

Now the meaning of these long checks are often simple, and the problem is that when you need to do something like that somewhere you could easily need to make the same check in related code all over the place. Obviously this isn't always the case, but it got me thinking about maybe being able to provide cfg aliases. Essentially you would just say:

should_have_x11=[cfg(all(any(feature = "sm-x11", all(unix, not(any(target_os = "macos", target_os = "android"))))))

Then anywhere you need to make that check you just do:

#[cfg(should_have_x11)]

This is much cleaner and makes it tons easier to make sure you are properly indicating the intent of the cfg check. Granted it also arguably adds a level of indirection that makes the developer not instantly able to tell when that code would be included, but I think that it would be worth it in situations and that the developer could make the call.

I guess this is something more for the rust internals forum or a pre-RFC or something like that, but I wanted to post here first to make sure there isn't anything similar already.

Now that I say that, I guess I could make a macro that does this. That wouldn't even need a compiler change so that is sounding very appealing, actually, as long as there isn't a major blocker for where you are allowed to expand syntax with a macro, which there might be depending on how #[cfg] like you want it.

2 Likes

You can make a build script that emits should_have_x11 cfgs in the right situations: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-cfg

4 Likes

I agree that this is a desirable feature.


FWIW, wih enough macromancy one can achieve this:

make_cfg_alias! {
    macro let (cfg_my_combination, cfg_not_my_combination) =
        cfg_and_cfg_not!(
            all(
                feature = "a",
                not(feature = "b"),
                feature = "c",
            ),
        )
    ;
}

#[macro_rules_attribute(cfg_my_combination!)]
fn main ()
{
    println!("You win!");
}

#[macro_rules_attribute(cfg_not_my_combination!)]
fn main ()
{
    println!("You lose.");
}

image

This is achieved by:

  1. having a macro define the two associated dummy macros depending on the cfg:

    macro_rules! make_cfg_alias {(
        @with_dollar![$dol:tt]
        macro let ($yes:ident, $no:ident) =
            cfg_and_cfg_not! $cfgs:tt
        $(;)?
    ) => (
        #[cfg $cfgs]
        macro_rules! $yes {($dol it:item) => ($dol it)}
        #[cfg $cfgs]
        macro_rules! $no {($dol it:item) => ()}
    
        #[cfg(not $cfgs)]
        macro_rules! $yes {($dol it:item) => ()}
        #[cfg(not $cfgs)]
        macro_rules! $no {($dol it:item) => ($dol it)}
    ); ($($t:tt)*) => (make_cfg_alias! { @with_dollar![$] $($t)* })}
    
  2. using #[macro_rules_attribute] to be able to use the defined macros as attributes.

    • otherwise one would have to use the macros as:

      cfg_my_combination! {
          fn main ()
          {
              ...
          }
      }
      

By using something like ::paste to concat identifiers into new ones, one would be able to reduce the "redundancy" at call site, but this involves an extra dependency that pulls the heavy ::syn crate, and leasds to a more complex / convoluted make_cfg_alias! macro definition, which is already quite unreadable because of its nested macro definitions...

4 Likes

Ooh, that's a good idea. Definitely a simple solution.

Haha, that is pretty crazy, but I was actually interested to see that, and may come back to that sometime. #[macro_rules_attribute] is awesome by the way, I was wishing I could do that yesterday.


OK, with a sort of combo of both your tips, I've got a solution I really like actually:

build.rs:

/// Create an alias for `#[cfg]` attributes to use
macro_rules! cfg_aliases {
    ($($alias:tt = $config:meta),* $(,)*) => {
        $(
            if cfg!($config) {
                println!("cargo:rustc-cfg={}", stringify!($alias));
            }
        )*
    };
}


fn main() {
    cfg_aliases! {
        // This is repetative ;)
        is_unix = unix,
        // More complicated
        should_have_x11 = all(any(feature = "sm-x11",
            all(unix, not(any(target_os = "macos", target_os = "android")))))
    }
}

main.rs:

fn main() {
    foo();
}

#[cfg(is_unix)]
fn foo() {
    println!("I'm on Linux!");

    #[cfg(should_have_x11)]
    println!("Enabling X11");
}

#[cfg(not(is_unix))]
fn foo() {
    println!("I'm on Windows, or, not Linux anyway!");
}

It works perfect. Thanks for the help!

This is actually a really nice pattern, maybe I'll re-post it in tutorials. :slight_smile:

6 Likes

Ah, the macro I used could still use some sophistication, because it doesn't allow you to have aliases reference aliases.

@Yandros I just realized what you were doing with the crazy @with_dollar! step in that macro. That is some awesome macro trickery.

Anyway, I'm really trying to find out if there is a way to make my simple cfg_aliases macro in the example above more sophisticated so that it can expand aliases that are inside of other aliases. That way you could do something like ( I changed the syntax a bit from above ):

cfg_aliases! {
  linux: target_os = "linux",
  not_linux: not(linux),
}

The point would be so that not_linux actually expands to not(target_os = "linux). I think I have a plan for the general parsing structure of the macro, but I've hit a snag that is illustrated in this playground:

macro_rules! cfg2 {
    ($($cfg:meta)*) => {
        macro_rules! cfgtest1 {
            () => {
                $($cfg)*
            };
        }

        cfg!(cfgtest1!())
    };
}

fn main() {
    cfg2!(unix);
}

The problem is this error:

error: expected 1 cfg-pattern
  --> src/main.rs:9:9
   |
9  |         cfg!(cfgtest1!())
   |         ^^^^^^^^^^^^^^^^^
...
14 |     cfg2!(unix);
   |     ------------ in this macro invocation

Now this example is contrived but the gist of the issue is that I need to feed the result of a macro-built macro to the cfg! macro, but the cfg! macro doesn't interpret the tokens output by the cfgtest1! macro, it actually just literally interprets cfgtest1. Is there any way around this?


Edit: I think I just realized the issue. It is that the cfgtest1 macro that is defined in the macro cannot be passed into another macro because it won't expand that macro until the macro that created it has expanded. :confused: OK, that leaves me in a pickle. Here is a full example of what I'm trying to accomplish, from macro input to code output:

input:

cfg_aliases! {
    wasm: target_arch = "wasm32",
    surfman: all(unix, feature = "surfman", not(wasm)),
}

output:

if cfg!(target_arch = "wasm32") { println!("cargo:rustc-cfg=wasm"); }
// Note how the `not(wasm)` at the end of our `surfman` alias was expanded to
// `not(target_arch = "wasm32")` here
if cfg!(all(unix, feature = "surfman", not(target_arch = "wasm32"))) {
    println!("cargo:rustc-cfg=linux");
}

Or maybe ( whichever is easier ):

#[cfg(target_arch = "wasm32)]
println!("cargo:rustc-cfg=wasm");
#[all(unix, feature = "surfman", not(target_arch = "wasm32"))]
println!("cargo:rustc-cfg=linux");

OK, breaking it down further it looks like it is impossible to get a macro to expand in either #[cfg()] attribute such as #[cfg(my_macro!())] or inside of the cfg!() macro such as cfg!(my_macro!()). This won't work:

macro_rules! breakme {
    ($config:meta, $($tokens:literal)*) => {
        if cfg!($config) {
            $($tokens)*;
        }
    };
}

macro_rules! meta {
    ($meta:meta) => {
        $meta
    };
}

fn main() {
    breakme!(meta!(not(unix)), "test");
}
error: no rules expected the token `!`
   --> src/main.rs:159:18
    |
140 | macro_rules! breakme {
    | -------------------- when calling this macro
...
159 |     breakme!(meta!(not(unix)), "test");
    |                  ^ no rules expected this token in macro call

No matter what I do it wants to interpet the ! literally in the cfg as opposed to expanding the macro. I tried all kinds of macro shenanigans and nothing worked so far. :confused:

I'm really wanting to avoid making a proc macro because I don't want to depend on proc_macro_hack or any extra dependencies, but it looks like that might be my only recourse.

I think I could manage to make a proc macro that doesn't require syn or quote at least, it would just be more primitive from an error reporting standpoint. I really don't want to push up the compile time. proc_macro_hack is fast enough to compile, though.

@zicklag a workaround is to define macros that do:

macro_rules! identity! {(
    $($input:tt)*
) => (
    $($input)*
)}

macro_rules! my_macro {(
    @pipe_to $out:ident !
    <input>
) => (
    $out! {
        <expansion>
    }
); (
    <input>
) => (
    my_macro! { @pipe_to identity!
        <input>
    }
)}

And then, instead of foo!(bar!(baz!( ... ))), you can do:

macro_rules! foo_bar { ($($input:tt)*) => (
    bar! { @pipe_to foo! $($input)* }
)}
baz! { @pipe_to foo_bar! ... }

it's not great, but maybe you can work your way using that.

1 Like

Thanks, I'll try it out and see if it helps. :slight_smile:

1 Like

Well, it looks like I'm still having trouble with the cfg! macro, I think I'll just to a proc macro with proc macro hack, which compiles real quick so I don't mind it much.

My question now is how you are supposed to return errors from a proc macro? Proc macro functions don't return Results so is it a specially formatted panic or something?

Maybe i'm missing something, but why wouldn't cfg_attr work?

cfg_attr conditionally applies an attribute based on a configuration predicate. What I'm trying to make is a macro that you can put in your build.rs script that will conveniently add configuration aliases so that you can just #[cfg(my_config_alias)] instead of having to do #[cfg(all(not(feature = "something"), unix, not(all(windows, feature = "somethingelse")))] or something long like that.

Ahh of course sorry, brain fart. One thing to look out for if you plan on using cross compilation here, is that build.rs cfg != target cfg.

In the tectonic_cfg_support crate we had to deal with this.

Oh, boy, I didn't think of that. So I could use the target_cfg! macro from tectonic_cfg_support instead of the cfg macro in my build.rs file?

Yeah, it doesn't support the full complexity of the whole cfg! macro, e.g. there are some expressions which are too complex for the macro IIRC cfg!(not(any(all(...),all(...))).

In these cases you have to drop back to working with the TARGET_CFG instance which has a API for getting at the environment variables, so it isn't a perfect emulation of cfg!.

The reason we didn't use proc-macros here is this requires(required?) dylibs, and so it wouldn't work for some native compilation targets.

The quick and dirty way is to panic!; the clean way is to expand to a call to compile_error! { "Error msg here" } spanned at the parsed location that caused an error.

  • this is made very easy with ::syn::Error's type, which includes a

    fn to_compile_error (self: Error) -> ::proc_macro2::TokenStream
    

    method, which you can chain to an .into() call to get a ::proc_macro::TokenStream.

Here is an example of usage: the implementation of ::syn::parse_macro_input!

1 Like

How do you span the error. I found the proc_macro::Span, but I couldn't figure out what to do with it.

The error will come from some element of the input that isn't as expected, and then you can either:

That being said, given that this kind of errors originate from parsing and (thus) validating the input of the macro, ::syn offers a convenient way to do this: the ::syn::parse::Parse trait:

1 Like

Thanks so much to all of you here, @sfackler, @Yandros, and @ratmice. With pieces of all of your input I was finally able to finish a cfg_aliases! macro_rules! macro that accomplishes what I needed!

https://crates.io/crates/cfg_aliases

After @ratmice brought up the target_cfg! macro from tectonic I found out about the CARGO_CFG environment variables. By checking those environment variables instead of trying to nest the use of the cfg! macro inside of a macro_rules! macro I was able to achieve what I was looking for without a proc macro.

Also the parser structure from the target_cfg! macro helped save me a lot of time because a whole chunk of that didn't need to be changed for use in the cfg_aliases! macro.

Additionally, part of the implementation needed the nested macro definition strategy that @Yandros demonstrated.

I'm very happy with the result and satisfied to be able to publish it as a crate that has no dependencies and takes 0.1 second to compile so that you don't have to have any shame in including it simply for convenience. :smile:


The massive macro could be better commented, and some of the comments were copied from the target_cfg! macro, but I would love any feedback on the code or the usage.

I'm not sure if I'm 100% settled on the macro syntax, but I think I like it and it is easy to look at. Initially the syntax didn't have the curly braces in it, but those ended up being required to delimit the token tree for the cfg expression. We could use {}, [], or (), but I think that curly's are the best in this case. Otherwise I thought of using an => instead of a :, but I think I like the : more.


Here's the full example:

Cargo.toml:

[build-dependencies]
cfg_aliases = "0.1.0-alpha.0"

build.rs:

use cfg_aliases::cfg_aliases;

fn main() {
    // Setup cfg aliases
    cfg_aliases! {
        // Platforms
        wasm: { target_arch = "wasm32" },
        android: { target_os = "android" },
        macos: { target_os = "macos" },
        linux: { target_os = "linux" },
        // Backends
        surfman: { all(unix, feature = "surfman", not(wasm)) },
        glutin: { all(feature = "glutin", not(wasm)) },
        wgl: { all(windows, feature = "wgl", not(wasm)) },
        dummy: { not(any(wasm, glutin, wgl, surfman)) },
    }
}

Now that we have our aliases setup we can use them just like you would expect:

#[cfg(wasm)]
println!("This is running in WASM");

#[cfg(surfman)]
{
    // Do stuff related to surfman
}

#[cfg(dummy)]
println!("We're in dummy mode, specify another feature if you want a smarter app!");

This greatly improves what would otherwise look like this without the aliases:

#[cfg(target_arch = "wasm32")]
println!("We're running in WASM");

#[cfg(all(unix, feature = "surfman", not(target_arch = "22")))]
{
    // Do stuff related to surfman
}

#[cfg(not(any(
    target_arch = "wasm32",
    all(unix, feature = "surfman", not(target_arch = "wasm32")),
    all(windows, feature = "wgl", not(target_arch = "wasm32")),
    all(feature = "glutin", not(target_arch = "wasm32")),
)))]
println!("We're in dummy mode, specify another feature if you want a smarter app!");

You can also use the cfg! macro or combine your aliases with other checks using all() , not() , and any() . Your aliases are genuine cfg flags now!

if cfg!(glutin) {
    // use glutin
} else {
    // Do something else
}

#[cfg(all(glutin, surfman))]
compile_error!("You cannot specify both `glutin` and `surfman` features");
8 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.