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

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.

3 Likes