Fast and Simple Rendering in Rust using Proc Macros
I’ve been working on a project called Thruster recently, and needed a way that a developer could reasonably use templates. Thruster is a middleware based web server written in Rust (get it, th-rust-er? I’m working on my tight 5 for amateur night at The Apollo,) and as such, I needed a way to load HTML templates and insert variables into them in a performant way. Rather than poking around the numerous existing libraries and choosing one made by someone I don’t know — stranger danger! — I decided to make it myself. This article is about that journey, the unbelievably thrilling adventures of writing my first
proc_macro_derive in Rust. Cue the Indiana Jones music.
The first thing you need to know, is that I’m not what I’d call a systems guy. I picked up rust because I wanted a newer language that was performance based, but I’m really more of a web developer. I can already hear your salt through the internet tubes already; “Why in the world would I take advice from a web developer? He probably just wants to talk about arrow functions and how his generation discovered functional programming.” Well you’re right. I do just want to talk about that, but I’m also in the middle of making avocado toast and my life coach said that I should concentrate on my chi instead of talking about Elm-lang.
The reason I mention that I’m a web developer, is that one of the first things I look for in a web framework is how complicated developing in the framework is and how easy a framework is to compose and move portions around. Sometimes I like to be able to just copy/pasta code I’ve already written in different projects without having to recalculate the trajectory to put a man on the moon.
The second thing you should know, is that Rust is great, and although my coworkers want to throw a shoe at my head if I even look like I’m about to talk about it’s memory safety or blazing speed, my love for it continues to burn brighter than a thousand suns.
Great, now that the confessional is out of the way, we can actually dig into the meat of the project;
proc_macro_derive . Procedural macros (and macros in general,) are fascinating creatures. Many developers from other languages will roll their eyes and remember every time they had to debug a mysterious macro, but Rust is a little different. From the docs:
…Rust has a hygienic macro system. Each macro expansion happens in a distinct ‘syntax context’, and each variable is tagged with the syntax context where it was introduced.
Here’s what I wanted to accomplish in for my macro:
- It should draw in a template from a file during compilation rather than at runtime.
- It is type safe, i.e. I won’t end up with a panic! or some strange
undefinedtype text in my rendered template during runtime.
- It is fast.
The first bullet point there, unfortunately, discounts just regular old macros, so I had to start digging into procedural macros. Macros that essentially have extra logic that can be brought in during the compilation process and actually alter the AST of the code.
Procedural Macros are special beasts, so to start, I needed to declare that my code is a special snowflake in the
proc-macro = true
I also included a few handy dandy packages for going from human code (oxymoron?) to a rust AST and back again,
syn = "0.13.1"
quote = "0.5.1"
Next I started the code for the actual macro. An important distinction here to make was the difference between the different types of procedural macros. I’ll just leave this here because someone already did the work for me in the Rust Dark Arts book: The Unstable Book.
Long story short — I decided to make a custom derive procedural macro! That means my code looked like this to start:
At this point I had a way to add custom implementations to a struct, exactly what you would want to use a derive for. That’s all well and good, but I still wanted to be able to load a template from a file. The question is, how in the world to get the name of the template to load?
Well, luckily for us, we’re in compiler land (much worse than Candy Land) so we can add extra attributes as well. Here’s the code with the added
TemplateName attribute for grabbing the template
Yes, I know the top is a bit of a mess. Like my garage and my desk at work, I’ll get to cleaning it up when I have some free time. What I’m doing here is iterating through the attributes on the struct that’s passed in in order to find the
TemplateName attribute. Then, I’m loading a file’s contents based on that string, and passing it into our token stream made by
The last step is doing the actual interpolation, which gets a little messy, but is relatively straightforward to do. The important thing to remember is that everything outside of the passed back contents from
quote! will be done at compile time, and vice versa. Note that the example above doesn’t actually include interpolation, instead, here’s a link to the published package’s source on GitHub.
You’ve done it. You’ve followed my scattered logic and forced rhetoric to the final scene where the faces melt off of the Nazis (you didn’t think I’d forgotten about the Indiana Jones reference in the first paragraph did you?) Procedural Macros are great in Rust; they’re type safe and hygienic, but they’re not a silver bullet. They have limits to their power for the safety of the developer, and the sanity of anyone who has to maintain your code. If there is a problem that’s decidable at compile time, however, they’re a pretty great tool.