Generate enum from string

I'm currently leraning macro_rules and one problem I want to solve is how to generate an enum from a string. Having this file

rice,0x00000009
wheat,0x00000010
...

I want to generate this

enum Resources {
    rice = 0x00000009,
    wheat = 0x00000010,
    ...
}

What I think the macro will look like

macro_rules! resources_enum_values {
     ($($key:ident)*) => {
         $({
            let row = $key.split(',');
            format!("{} = {},", row[0], row[1])
         })*
     }
 }

macro_rules! generate_resources_enum {
     ($data: ident) => {
        enum Resources {
            resources_enum_values($data.lines().collect())
        }
     };
 }

const RESOURCES: &'static str = &include_str!("resources");
generate_resources_enum!(RESOURCES);

But somehow I fail to understand how to execute something inside the macro to generate
what I want, my understanding is that I need to split the string line by line, then split the result using the comma as separator, take the first 2 values and format the output as $key = $value, but seems that I cannot execute any code inside the macro, tried to execute a block and return the result but get syntax errors, maybe what I'm trying to accomplish is out of the scope of macro_rules or my approach is wrong.

Thanks.

macro_rules! can't perform arbitrary code. Instead, it uses a declarative style syntax:

macro_rules! NAME {
    (PATTERN_1) => OUTPUT_1;
    (PATTERN_2) => OUTPUT_2;
    ...
}

When you call it, it uses the patterns to parse the input, and once one of them matches, this is used to generate the output. If you were to stick the contents of the file directly inside the macro invocation, you could easily parse it:

macro_rules! generate_resources_enum {
    ( $($key:ident , $value:literal)* ) => {
        enum Resources {
            $( $key = $value, )*
        }
    }
}

generate_resources_enum!{
    rice,0x00000009
    wheat,0x00000010
}

However, there is no way to parse it with patterns when the data is in another file. In fact, there are numerous problems:

  • Macros are only given the interior of their invocation as input. I.e. when you call it with generate_resources_enum!(RESOURCES), all it gets is a single token, the identifier RESOURCES. It has no way to determine the value of this constant. (It doesn't even know that RESOURCES is supposed to name a constant!)
  • Even if you called it with generate_resources_enum!(include_str!(resources)), it would only get the token tree:
    [
        Ident("include_str"),
        Symbol::Not,
        TokenTree(Delimiter::Paren, [
            Literal::Str("resources"),
        ]),
    ]
    
    There is no way to have the include_str! macro expand first.
  • Even if there was a way, a string literal is a single token. All of the matchers available to macro_rules (ident, pat, tt, ...) match one or more complete tokens. In other words, there's no way for macro_rules to pull apart the data inside a string literal.

There is another kind of macro called "procedural macros." These are written in rust code, allowing you to do arbitrary things to the input. This solves the third bullet point, but not the first two. You could try to read the file directly from your procedural macro, but I'd be worried about how relative paths are resolved.

So if this data needs to be in a separate file, it looks like any form of macro is no good. You should probably instead write a cargo build script that reads the file and generates an .rs file containing the enum.

1 Like

Thanks for taking the time to answer me, it seems that the build script is better suited for my use case as you pointed out, I didn't know about it.

There is https://github.com/Peternator7/strum/ crate.

2 Likes

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