While working on my Rust Dominion simulator, I came across the problem of how to represent various cards in memory. My initial approach was to use enums with fields, but that approach had to be abandoned in order to support multi-type cards like Great Hall. My second approach was to convert the type field to a bitstring, which works, but caused each card to define much more data than it needed to (every card had to declare a money value, action, and victory point value, even though the vast majority of cards only have one of the three). My third approach was to keep the bitstring and utilize the
Default trait to avoid unnecessary value declarations, but I haven’t figured out how to get that to work properly in a static context (if it does). My fourth approach is to use macros, which is what I am going to show you here.
Macros in Rust are pretty awesome, even though they’re still considered unstable. They allow for C preprocessor-style metaprogramming, but they operate on expressions and identifiers rather than plain text, making them much less error-prone.
To demonstrate the transformative power of a macro, I’ll first show you what a static card declaration looks like without one. Here are the types involved:
Notice how the
CardDef struct needs to keep track of a bunch of different pieces of data, even though each card will only use a portion of it. All cards need to define a name, cost, and type, but the other three are usually mutually exclusive. Unfortunately (though this is ultimately a good thing in the name of program safety), every field needs to be defined for every card.
Here’s an example of a card declaration using this setup, where
do_smithy is a function that matches the
ActionFunc signature (and presumably has the effect of drawing three cards):
But with MACROS, we can instead choose to write it like this:
action! macro returns a new Card, and takes care of the following things for us:
vpto the default 0.
- Fills in the provided values for
action(in this case, “Smithy”, 3, and do_smithy).
The reason that we’re allowed to define a macro that looks like this is that the macro definition syntax supports defining arguments of specific types, and whatever isn’t an argument is treated literally. In the case of
action!, the words “costs” and “and calls” are effectively just elaborate commas.
Here’s what the macro definition looks like (requires a
#[feature(macro_rules)]; declaration in the crate root to work:
The macro syntax is defined on the second line, and the resulting expansion is declared on the third. Rust’s syntax here is actually pretty intuitive to read, so I’ll defer to the official Rust docs for more information on macro syntax.
Similar macros for other types can easily be defined, though I haven’t yet settled on an ideal approach for tackling cards like Great Hall. Here are some other examples:
What sort of atrocities can you create with macros?
Follow Damien Radtke on Twitter: www.twitter.com/damienradtke