A short getting started on derive macros guide in Rust.
Important
This assumes you're familiar with Rust's declarative macros. Or at least
knowledgeable of programming language syntax and meaning of words like:
identifier
and expression
.1 2 3 4 5
-
A quick introduction to the three main types of declarative macros6:
- Function-like -
my_function_like_macro!(...)
(Not to be confused with declarative macros). - Derive -
#[derive(MyDeriveMacro)]
(We'll focus on this one). - Attribute -
#[my_attribute_macro]
.
- Function-like -
-
How to manage your thought process about the structuring of your crates and the structure of the macro.
-
Understanding the process of creating a procedural macro and how it varies from declarative macros.
-
Introduction to the common tools to achieve the task: the
syn
andquote
crates.
Similarly to how compilers work, procedural macros work by taking some input, doing something with it, then generating new input for our main program. This is also the reason procedural macros need a separate crate in order to work.
Fortunately, most of the heavy lifting (parsing and generating Rust code) is
already done with the help of syn
and quote
. This means that for Rust code
generation, we can focus on the logic behind what we want to achieve more than
making a parser for the complex syntax of the language.
- Syn handles the parsing (usually as
syn::DeriveInput
for derive macros). - We work with the parsed data.
- Our work gets tokenized by the
quote::quote!
macro.
An example project using a common pattern called: Reflective programming
. The derive
macro will work on structs and enums and add a method to them called:
get_fields()
for structs and get_variants()
for enums.
"How does this reflect in the real world?"
Retrieving field names is a commonly used thing when we want to represent a
Rust structure in a different format. This is the default way of
serde
(serde_derive/src/pretend.rs#64-76)
to serialize your struct fields and enum variants.
Here's an example of how serde
would serialize a User
object defined like so:
use proc_macro_example_derive::Reflective;
#[derive(serde::Serialize, Reflective)]
struct User {
pub user_id: u64,
pub username: String,
pub age: u32,
}
let some_user = User {
user_id: 1234,
username: String::from("Harry"),
age: 41,
};
let fields = User::get_fields();
// ^--------------- How convenient. This is a good example of how this macro can be used
// to streamline the testing process.
let expected = format!(
r#"{{"{}":{},"{}":"{}","{}":{}}}"#,
fields[0], some_user.user_id, fields[1], &some_user.username, fields[2], some_user.age
);
assert_eq!(serde_json::to_string(&some_user).unwrap(), expected);
If you've used yew
,
leptos
or any other web
development library, then you'd know you can parse any TokenStream
into a
Rust TokenStream
. This example doesn't go that in-depth with what you can do
since in practice it's possible to make a programming language with Rust's proc
macros. In fact, people have also done that with a
Python interpreter written in Rust
or Python bindings in Rust.
The possibilities are endless and this example just scratches the surface of procedural macros.
Some other projects whose procedural macros you can check out are:
-
Comprehending Proc Macros by Logan Smith (also the main inspiration for this tutorial).
This repository is public domain and dual licensed with the CC0 1.0 Universal license or The Unlicense lisence.
You're free to choose which one to use.