Supporting or evaluating #[cfg(...)] in proc macro parameters?

I'm trying to build out a proc macro that takes input in the following form to build a data structure:

ecs_world! {
    ecs_archetype!(
        MyArchetype1,
        ComponentA,
        ComponentB,
        #[cfg(server)] ComponentC,
    );

    #[cfg(client)]
    ecs_archetype!(
        MyArchetype2,
        ComponentD,
        ComponentE,
    );
}

I need to support #[cfg(...)] attributes in two different positions: one on the top level of an inner macro, and one on any of its parameters. I also want the proc macro to be aware of the shape of the final data structure after cfgs are evaluated in order to perform certain queries later that require knowledge of how the data structure was built at the callsite.

I'm grappling with how to do this in a reasonable way. I know the traditional way to handle this is for the proc macro output to generate all possible outcomes ahead of time, but I can't do that here because that space explodes combinatorically due to potential nesting of arbitrarily unique cfg arguments.

Fundamentally, it seems like I need a way for the proc macro to be able to evaluate the #[cfg] attributes in the scope of the calling crate. I don't believe there's any formal support for doing so, but I'm aware of a few tricks for achieving it. One of them is to create essentially a macro callback of the form:

#[cfg(foo)]
macro_rules! check_for_foo {
    () => foo_true!(),
}
#[cfg(not(foo))]
macro_rules! check_for_foo {
    () => foo_false!(),
}
check_for_foo!()

This requires the macro to be a freestanding macro though, and can't be done in the context of another macro parse since you can't force eager evaluation.

I could create a stateful proc-macro here, where I have nested and ordered proc macros, some of which use these callbacks to switch their execution, like so:

#[cfg(server)]
macro_rules! maybe_step_2 {
    (($tail:tt)*) => { step_2!($($tail)*) }
}

#[cfg(not(server))]
macro_rules! maybe_step_2 {
    (($tail:tt)*) => { $($tail)* }
}

step_1!(maybe_step_2!(step_3!()));

However, I know proc macro ordering is very loosely defined and brittle. Are there any tricks or rules I could take advantage of in this case? It seems that currently in practice function-like proc macro execution is actually performed in linear order for a given file, and some crates already take advantage of this implementation-specific behavior. I'm willing to do so as well if all else fails, but I'd like something a little more robust if possible. At the very least I'd rather do this than use a build.rs script for example.

Any ideas on how I could move forward on this?

1 Like

For anyone who might have to solve this later, I think I've found a workable solution that doesn't depend on stateful macros. The process works in these steps:

  1. In your user-facing proc macro, collect all the #[cfg(...)] attribute predicates in your arguments and build a unique list of them with a stable order.

  2. Have that user-facing proc macro generate an induction chain of macro_rules macros to serially evaluate each, like so:

  #[cfg(/* PREDICATE GOES HERE */)]
  macro_rules! check_cfg_0{
    (($($bools:expr), *), $($args:tt)*) => {
      check_cfg_1!(($($bools,)* true), $($args)*);
    }
  }
  #[cfg(not(/* PREDICATE GOES HERE */))]
  macro_rules! check_cfg_0{
    (($($bools:expr), *), $($args:tt)*) => {
      check_cfg_1!(($($bools,)* false), $($args)*);
    }
  }

Notice that each check_cfg_N generates the call to the N+1 version of the macro. The last macro in the check chain should go to a special check_cfg_final! proc macro (instead of e.g. check_cfg_1 in the example above).

Additionally, have your user-facing macro kickstart the induction process like so:

check_cfg_0!((), args)

where args is the same args that were initially passed in to this user-facing macro.

  1. Write your check_cfg_final! proc macro to take arguments in the form of:
check_cfg_final!((true, false, true), args)

In this final macro, parse that boolean tuple, and also re-parse your data (this is a little slow -- there's certainly room for improvement here).

  1. Regenerate your unique cfg predicate list from step 1 (this is why it's important to have a stable order) from the re-parsed arguments. You can now zip the cfg predicate list with the input boolean tuple and build a hashmap lookup of predicate states. When you're using the data you re-parsed you can check if any parsed #[cfg(...)] attributes are enabled or not in the downstream crate.

The way this works is that each check_cfg_N generates the next check macro for the next predicate, branching based on if #[cfg(predicate)] or #[cfg(not(predicate))] is currently true. Because each macro inductively generates the next macro call, this forces them to be executed sequentially, and you preserve the state of the evaluation from the previous invoke, without side-effects, by adding it to the parameter list.

It's ugly, but it works, and it doesn't break any of Rust's rules for proc macros as far as I can tell.

2 Likes

thanks for sharing the trick. this really shows the limitation of macros that has only access to syntactical information. I really miss clojure (I don't have experience in other lisp languages other than a tidbit of emacs listp years ago).

if you want to use nightly features, you can use TokenStream::expand_expr on cfg!(<insert-cfg-condition-here>)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.