THE BLOG

Featuring fresh takes and real-time analysis from HuffPost's signature lineup of contributors

Damien Radtke Headshot

Using and Abusing Macros

Posted: Updated:


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:

// ActionFunc defines the method signature for functions that are called
// to handle the effects of Action cards.
pub type ActionFunc = fn(&mut Player, &[ActionInput]);

// Card is defined as a static pointer to a CardDef struct.
pub type Card = &'static CardDef;

// CardType is an enum holding the different values of the type bitstring.
#[deriving(Hash,Eq,Ord)]
pub enum CardType {
    NoType  = 0u,
    Money   = 1u,
    Victory = 2u,
    Action  = 4u,
    Curse   = 8u,
}

// CardDef defines a card, and is where all of the action is.
#[deriving(Hash,Ord)]
pub struct CardDef {
    name:   &'static str,
    cost:   uint,
    value:  uint,
    vp:     uint,
    action: *ActionFunc,
    typ:    CardType
}

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):

pub static SMITHY: Card = &'static CardDef {
    name:   "Smithy",
    cost:   3,
    value:  0,
    vp:     0,
    action: &do_smithy,
    typ:    Action,
};

But with MACROS, we can instead choose to write it like this:

pub static SMITHY: Card = action! { "Smithy" costs 3 and calls do_smithy };

The action! macro returns a new Card, and takes care of the following things for us:

  1. Sets value and vp to the default 0.
  2. Sets typ to Action.
  3. Fills in the provided values for name, cost, and 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:

macro_rules! action(
    ($name:expr costs $cost:expr and calls $action:ident) => {
        &'static CardDef { name: $name, cost: $cost, value: 0, vp: 0, action: &$action, typ: Action }
    }
)

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:

pub static WITCH: Card = action! { "Witch" costs 5 and calls do_witch };
pub static COPPER: Card = money! { "Copper" costs 0 and gives 1 buying power };
pub static ESTATE: Card = victory! { "Estate" costs 2 and gives 1 victory points };

What sort of atrocities can you create with macros?