0

Here is a problem I am trying to solve. I have multiple procedural macro functions that generate tables of pre-computed values. Currently my procedural macro functions take parameters in the form of literal integers. I would like to be able to pass these parameters from a configuration file. I could re-write my functions to load parameters from macro themselves. However, I want to keep configuration from a top level crate, like in this example:

top-level-crate/
    config/
        params.yaml
    macro1-crate/
    macro2-crate/
     

Since the input into a macro function is syntax tokens not run-time values, I am not able to load a file from top-level-crate and pass params.

    use macro1_crate::gen_table1;
    use macro2_crate::gen_table2;

    const TABLE1: [f32;100] = gen_table1!(500, 123, 499);
    const TABLE2: [f32;100] = gen_table2!(1, 3);

    fn main() {
       // use TABLE1 and TABLE2 to do further computation.
        
    }

I would like to be able to pass params to gen_table1 and gen_table2 from a configuration file like this:


    use macro1_crate::gen_table1;
    use macro2_crate::gen_table2;
   
    // Load values PARAM1, PARAM2, PARAM3, PARAM4, PARAM5

    const TABLE1: [f32;100] = gen_table1!(PARAM1, PARAM2, PARAM3);
    const TABLE2: [f32;100] = gen_table2!(PARAM4, PARAM5);

    fn main() {
       // use TABLE1 and TABLE2 to do further computation.
        
    }

The obvious problem is that PARAM1, PARAM2, PARAM3, PARAM4, PARAM5 are runtime values, and proc macros rely on build time information to generate tables.

One option I am considering is to create yet another proc macro specifically to load configuration into some sort of data-structure built using quote! tokens. Then feed this into macros. However, this feels hackish and the configuration file needs to be loaded several times. Also the params data structure need to be tightly coupled across macros. The code might look like this:


    use macro1_crate::gen_table1;
    use macro2_crate::gen_table2;
   
    const TABLE1: [f32;100] = gen_table1!(myparams!());
    const TABLE2: [f32;100] = gen_table2!(myparams!());

    fn main() {
       // use TABLE1 and TABLE2 to do further computation.
        
    }

Any improvements or further suggestions?

Dilshod Tadjibaev
  • 1,035
  • 9
  • 18
  • 1
    Unfortunately, I don't quite understand the problem here. Proc macros are not sandboxed currently and you can read files from anywhere, so using `$CARGO_MANIFEST_DIR/../config` should be fine. But you also mention runtime values: if the configuration is something that is only available at runtime, not compile time, then there is no way your procedural macros can get access to it, as they are run at compile time. Could you maybe edit your question to clarify the actual problem and what exactly you already tried? – Lukas Kalbertodt May 16 '21 at 09:46
  • I updated my question with examples and details of the problem. Also I explained a solution I was considering. Please let me know if you need more clarification. Thanks. – Dilshod Tadjibaev May 16 '21 at 15:31

1 Answers1

1

gen_table1!(myparams!()); won't work: macros are not expanded from the inside out, like function calls. Your gen_table1 macro will receive the literal token stream myparams ! () and won't be able to evaluate this macro, thus not having access to the "return value" of myparams.

Right now, I only see one real way to do what you want: load the parameters from the file in gen_table1 and gen_table2, and just pass the filename of the file containing the parameters. For example:

const TABLE1: [f32; 100] = gen_table1!("../config/params.yaml");
const TABLE2: [f32; 100] = gen_table2!("../config/params.yaml");

Of course, this could lead to duplicate code in these two macros. But that should be solvable with the usual tools: extract that parameter loading into a function (in case both macros live in the same crate) or into an additional utility crate (in case the two macros live in different crates).


You also keep mentioning the term "runtime values". I think you mean "a const value, not a literal" and that you are referring to something like this:

const PARAM1: u32 = load_param!();
const TABLE1: [f32; 100] = gen_table1!(PARAM1); // <-- this does not work as expected!

Because here, again, your macro receives the literal token stream PARAM1 and not the value of said parameter.

So yes, I think that's what you mean by "runtime value". Granted, I don't have a better term for this right now, but "runtime value" is misleading/wrong because the value is available at compile time. If you were talking about an actual runtime value, i.e. a value that is ONLY knowable at runtime AFTER compilation is already done, then it would be impossible to do what you want. That's because proc macros run once at compile time, and never at runtime.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Thank you for your answer. Good catch regarding nested macro calls and "runtime value" terminology. Your solution is probably the only way to solve it. However, there is one limitation I am facing with this solution. In cases when I need to repeat macro calls with different parameters, this solution will not work. Lets say I need to call gen_table1 three times with different params for initializing array const. I suppose I can update my gen_table1 to account for this case. I was just hoping I can modularize my code into macro functions. I will except your answer if no alt answers. – Dilshod Tadjibaev May 16 '21 at 17:02
  • Can't you just have three config files, one for each `gen_table1` invocation? And then pass different paths to the three invocations? Alternatively, you could pass a second parameter to `gen_table!` that describes a key to use from the config file or sth like that? – Lukas Kalbertodt May 16 '21 at 17:09
  • I guess it's possible too, which kinda lead me to another idea: keep one file but separate configuration by groups and pass group name as string literal to macro call, like this: `gen_table1!("../config/params.yaml", "group_name1");` – Dilshod Tadjibaev May 16 '21 at 18:44