Blank Chisui
Blank Chisui

Reputation: 1073

Is it possible to calculate a C-style `enum` value using a macro?

I have a huge list of four letter identifiers. I want an enum constant for each of them. The value of the constant should be the ASCII identifier as u32.

enum E {
    Elem = my_macro!(Elem),
}

Since the macro will simply be replaced by the resulting AST the compiler will see a non constant expression and raise an error.

Is this possible, or do I have to generate write the constants out explicitly?

Upvotes: 2

Views: 75

Answers (1)

Shepmaster
Shepmaster

Reputation: 430665

Kind of, yes, but remember that macros really only allow you to remove drudgery, not really add new features.

Start by writing the code you want without macros. This lets you see what is possible. For example, my exploring went like this:

#[repr(u32)]
enum Foo {
    Start = (1 << 24 | 2 << 16 | 3 << 8 | 4) as u32,
    Start2 = ((b'1' as u32) << 24 | 2 << 16 | 3 << 8 | 4) as u32,
    // the index operation on const values is unstable
    //Start3 = ((b"1"[0] as u32) << 24 | 2 << 16 | 3 << 8 | 4) as u32,
}

Sadly, we cannot use the last form as indexing into a constant isn't a constant expression (yet). As far as I can tell, the best we can do is Start2. Next, repeat the pattern a few times to see where the redundancy occurs:

#[repr(u32)]
enum Foo {
    Start = ((b'S' as u32) << 24 | (b'T' as u32) << 16 | (b'R' as u32) << 8 | (b'T' as u32)) as u32,
    End   = ((b' ' as u32) << 24 | (b'E' as u32) << 16 | (b'N' as u32) << 8 | (b'D' as u32)) as u32,
}

Now you are primed to create a macro:

macro_rules! tagged_ascii_headers {
    (enum $name:ident {
        $($var:ident = $v1:expr, $v2:expr, $v3:expr, $v4:expr,)*
    }) => {
        #[repr(u32)]
        enum $name {
            $($var = (($v1 as u32) << 24 | ($v2 as u32) << 16 | ($v3 as u32) << 8 | $v4 as u32),)*
        }
    }
}

tagged_ascii_headers! {
    enum Foo {
        Start = b'S', b'T', b'R', b'T',
        End   = b' ', b'E', b'N', b'D',
    }
}

Then you can play with the macro syntax to find something that looks nice. I got down to

macro_rules! tagged_ascii_headers {
    (enum $name:ident {
        $($var:ident = $v1:tt $v2:tt $v3:tt $v4:tt,)*
    }) => {
        #[repr(u32)]
        enum $name {
            $($var = (($v1 as u32) << 24 | ($v2 as u32) << 16 | ($v3 as u32) << 8 | $v4 as u32),)*
        }
    }
}

tagged_ascii_headers! {
    enum Foo {
        Start = 'S' 'T' 'R' 'T',
        End   = ' ' 'E' 'N' 'D',
    }
}

This is a bit nicer, but ultimately you might need more constant evaluation to be available. If arrays could be indexed, you could evolve to something like

tagged_ascii_headers! {
    enum Foo {
        Start = b"STRT",
        End   = b" END",
    }
}

Since the macro will simply be replaced by the resulting AST

This is true

the compiler will see a non constant expression

This is half-true. For example, this compiles just fine:

macro_rules! foo {
    () => { 42 }
}

enum Foo {
    Start = foo!(),
}

So really, the macro is irrelevant to the const-ness, it's all about what the macro expands to.


You could also move to a build script:

const THINGS: &'static [(&'static str, &'static [u8; 4])] = &[
    ("Start", b"STRT"),
    ("End",   b" END"),
];

fn main() {
    println!("#[repr(u32)]");
    println!("enum Foo {{");
    for &(name, code) in THINGS {
        let code = (code[0] as u32) << 24 | 
                   (code[1] as u32) << 16 |
                   (code[2] as u32) << 8  |
                    code[3] as u32;
        println!("    {} = {},", name, code);
    }
    println!("}}");
}

You'd want to write that to a file instead of standard output, then include the generated file from the production code. A build script would also allow you to have some external file that defines all the names / codes, if that was valuable.

Upvotes: 5

Related Questions