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.