Something C++ Can't Do


I have a library I'm writing in Rust which deals with ASCII-ish graphics, so, I need a way to represent that, and I want it to be easy to deal with. Let's start with a basic struct:

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Cell {
    /// The character to display
    pub ch: u8,
    /// The foreground, used tor white pixels in the bitmap
    pub fg: Color,
    /// the background, used for black pixels in the bitmap
    pub bg: Color,
}

A Cell is a combination of a byte for which character to draw and two colors for how to draw it. Simple enough.

But it might make sense to say "these two colors are the colors we use for a wall, whatever character that wall is." Or "this character is a potion bottle, but which actual potion it is determines the colors." So it makes sense to have partial instances of this struct, with just the ch and the fg or something.

Ordinarily I might reach for Option here, but that's not a great solution because how do I say "this function draws a Cell but only if all three members are present?" It's a little ugly. So instead I define some more types:

pub struct Fg(pub Color);
pub struct Bg(pub Color);
pub struct Char(pub u8);
pub struct FgBg { fg: Color, bg: Color }
pub struct FgChar { fg: Color, ch: u8 }
pub struct BgChar { bg: Color, ch: u8 }

Now I can represent each possible combination of things as a different type, and write functions like "draw a wall in this color combination (but the function decides that character to use)." I need to start making some ways to convert between these, though: to build an FgBg from an Fg and a Bg, for example. And because there are six types, I'm looking at a pretty ugly combinatorial explosion, hundreds of lines of boilerplate.

There are two ways to write macros in Rust and this is the simpler one: it's just pattern expansion; you give Rust a pattern to find (which must conform to some rules) and then how to expand it into Rust code:

macro_rules! property_sum {
    ($a:ty { $($afield:tt => $sumafield:ident),+ }, $b:ty { $($bfield:tt => $sumbfield:ident),+ } => $sum:ty ) => {
        impl Add<$a> for $b {
            type Output = $sum;
            fn add(self, rhs: $a) -> Self::Output {
                Self::Output {
                    $($sumafield: rhs.$afield,)+
                    $($sumbfield: self.$bfield,)+
                }
            }
        }
        // elided add-b-for-a
    };
}

This creates a new "macro rule" called property_sum. That rule expects something kind of like this:

property_sum!(Fg { 0 => fg }, Bg { 0 => bg } => FgBg);

You can see how, if you squint, the patterns sort of match: $a:ty matches with Fg, then in braces 0 is $afield:tt and fg is $sumafield:ident and so on. Part of the pattern is wrapped in $( ... ),+ which means "a comma-separated list of [whatever]." I won't go into all of it here, but you can read a very straightforward guide in the Rust book: Macros by Example.

The code it expands to is a simple implementation of Add<T>, which is how you overload the + operator in Rust. We know what the output type is (FgBg) and we create one, initializing all its fields as we were told to by the mappings: Fg.0 goes into FgBg.fg; Bg.0 goes into FgBg.bg.

I elided a second implementation so that bg + fg also works; addition is left-associative.

With this, instead of boilerplate, we get field mappings:

property_sum!(Fg { 0 => fg }, Bg { 0 => bg } => FgBg);
property_sum!(Fg { 0 => fg }, Char { 0 => ch } => FgChar);
property_sum!(Bg { 0 => bg }, Char { 0 => ch } => BgChar);
property_sum!(Fg { 0 => fg }, BgChar { bg => bg, ch => ch } => Cell);
property_sum!(Bg { 0 => bg }, FgChar { fg => fg, ch => ch } => Cell);
property_sum!(Char { 0 => ch }, FgBg { fg => fg, bg => bg } => Cell);

This defines the entire arithmetic of the seven types: you can add an Fg and a Char to get an FgChar; then add a Bg to that to get a Cell, and so on.

There are two more macros in this file:

  • apply_fields! lets me bitwise-or a little type to modify a larger type, so for example:
        let mut a = Fg(RED) + Bg(BLUE) + Char(0x32u8);
        a |= Char(65);
        a |= Bg(YELLOW);
        assert_eq!(a, Fg(RED) + Bg(YELLOW) + Char(65));
  • into_properties! lets me extract parts of a type, so that I can extract just an FgBg from a Cell, for example:
        let c = Fg(RED) + Bg(WHITE) + Char(0x65u8);
        assert_eq!(FgBg::from(c), Fg(RED) + Bg(WHITE));

The whole file, which you can see here, is about 170 lines, 40 of which are tests. Expanded it would be much, much more, and probably have copy-paste errors.