Refactoring some repetitive code to a Rust macro

- Tags: librsvg, rust

I have started porting the code in librsvg that parses SVG's CSS properties from C to Rust. Many properties have symbolic values:

stroke-linejoin: miter | round | bevel | inherit

stroke-linecap: butt | round | square | inherit

fill-rule: nonzero | evenodd | inherit

StrokeLinejoin is the first property that I ported. First I had to write a little bunch of machinery to allow CSS properties to be kept in Rust-space instead of the main C structure that holds them (upcoming blog post about that). But for now, I just want to show how this boiled down to a macro after refactoring.

First cut at the code

The stroke-linejoin property can have the values miter, round, bevel, or inherit. Here is an enum definition for those values, and the conventional machinery which librsvg uses to parse property values:

#[derive(Debug, Copy, Clone)]
pub enum StrokeLinejoin {
    Miter,
    Round,
    Bevel,
    Inherit,
}

impl Parse for StrokeLinejoin {
    type Data = ();
    type Err = AttributeError;

    fn parse(s: &str, _: Self::Data) -> Result<StrokeLinejoin, AttributeError> {
        match s.trim() {
            "miter" => Ok(StrokeLinejoin::Miter),
            "round" => Ok(StrokeLinejoin::Round),
            "bevel" => Ok(StrokeLinejoin::Bevel),
            "inherit" => Ok(StrokeLinejoin::Inherit),
            _ => Err(AttributeError::from(ParseError::new("invalid value"))),
        }
    }
}

We match the allowed string values and map them to enum values. No big deal, right?

Properties also have a default value. For example, the SVG spec says that if a shape doesn't have a stroke-linejoin property specified, it will use miter by default. Let's implement that:

impl Default for StrokeLinejoin {
    fn default() -> StrokeLinejoin {
        StrokeLinejoin::Miter
    }
}

So far, we have three things:

  • An enum definition for the property's possible values.
  • impl Parse so we can parse the property from a string.
  • impl Default so the property knows its default value.

Where things got repetitive

The next property I ported was stroke-linecap, which can take the following values:

#[derive(Debug, Copy, Clone)]
pub enum StrokeLinecap {
    Butt,
    Round,
    Square,
    Inherit,
}

This is similar in shape to the StrokeLinejoin enum above; it's just different names.

The parsing has exactly the same shape, and just different values:

impl Parse for StrokeLinecap {
    type Data = ();
    type Err = AttributeError;

    fn parse(s: &str, _: Self::Data) -> Result<StrokeLinecap, AttributeError> {
        match s.trim() {
            "butt" => Ok(StrokeLinecap::Butt),
            "round" => Ok(StrokeLinecap::Round),
            "square" => Ok(StrokeLinecap::Square),
            "inherit" => Ok(StrokeLinecap::Inherit),

            _ => Err(AttributeError::from(ParseError::new("invalid value"))),
        }
    }
}

Same thing with the default:

impl Default for StrokeLinecap {
    fn default() -> StrokeLinecap {
        StrokeLinecap::Butt
    }
}

Yes, the SVG spec has

default: butt

somewhere in it, much to the delight of the 12-year old in me.

Refactoring to a macro

Here I wanted to define a make_ident_property!() macro that would get invoked like this:

make_ident_property!(
    StrokeLinejoin,
    default: Miter,

    "miter" => Miter,
    "round" => Round,
    "bevel" => Bevel,
    "inherit" => Inherit,
);

It's called make_ident_property because it makes a property definition from simple string identifiers. It has the name of the property (StrokeLinejoin), a default value, and a few repeating elements, one for each possible value.

In Rust-speak, the macro's basic pattern is like this:

macro_rules! make_ident_property {
    ($name: ident,
     default: $default: ident,
     $($str_prop: expr => $variant: ident,)+
    ) => {
        ... macro body will go here ...
    };
}

Let's dissect that pattern:

macro_rules! make_ident_property {
    ($name: ident,
//   ^^^^^^^^^^^^ will match an identifier and put it in $name

     default: $default: ident,
//            ^^^^^^^^^^^^^^^ will match an identifier and put it in $default
//   ^^^^^^^^ arbitrary text

     $($str_prop: expr => $variant: ident,)+
                       ^^ arbitrary text
//   ^^ start of repetition               ^^ end of repetition, repeats one or more times

    ) => {
        ...
    };
}

For example, saying "$foo: ident" in a macro's pattern means that the compiler will expect an identifier, and bind it to $foo within the macro's definition.

Similarly, an expr means that the compiler will look for an expression — in this case, we want one of the string values.

In a macro pattern, anything that is not a binding is just arbitrary text which must appear in the macro's invocation. This is how we can create a little syntax of our own within the macro: the "default:" part, and the "=>" inside each string/symbol pair.

Finally, macro patterns allow repetition. Anything within $(...) indicates repetition. Here, $(...)+ indicates that the compiler must match one or more of the repeating elements.

I pasted the duplicated code, and substituted the actual symbol names for the macro's bindings:

macro_rules! make_ident_property {
    ($name: ident,
     default: $default: ident,
     $($str_prop: expr => $variant: ident,)+
    ) => {
        #[derive(Debug, Copy, Clone)]
        pub enum $name {
            $($variant),+
//          ^^^^^^^^^^^^^ this is how we invoke a repeated element

        }

        impl Default for $name {
            fn default() -> $name {
                $name::$default
//              ^^^^^^^^^^^^^^^ construct an enum::variant

            }
        }

        impl Parse for $name {
            type Data = ();
            type Err = AttributeError;

            fn parse(s: &str, _: Self::Data) -> Result<$name, AttributeError> {
                match s.trim() {
                    $($str_prop => Ok($name::$variant),)+
//                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expand repeated elements

                    _ => Err(AttributeError::from(ParseError::new("invalid value"))),
                }
            }
        }
    };
}

Getting rid of duplicated code

Now we have a macro that we can call to define new properties. Librsvg now has this, which is much more readable than all the code written by hand:

make_ident_property!(
    StrokeLinejoin,
    default: Miter,

    "miter" => Miter,
    "round" => Round,
    "bevel" => Bevel,
    "inherit" => Inherit,
);

make_ident_property!(
    StrokeLinecap,
    default: Butt,   // :)

    "butt" => Butt,
    "round" => Round,
    "square" => Square,
    "inherit" => Inherit,
);

make_ident_property!(
    FillRule,
    default: NonZero,

    "nonzero" => NonZero,
    "evenodd" => EvenOdd,
    "inherit" => Inherit,
);

Etcetera. It's now easy to port similar symbol-based properties from C to Rust.

Eventually I'll need to refactor all the crap that deals with inheritable properties, but that's for another time.

Conclusion and references

Rust macros are very powerful to refactor repetitive code like this.

The Rust book has an introductory appendix to macros, and The Little Book of Rust Macros is a fantastic resource that really dives into what you can do.