|
| 1 | +% Procedural Macros (and custom Derive) |
| 2 | + |
| 3 | +As you've seen throughout the rest of the book, Rust provides a mechanism |
| 4 | +called "derive" that lets you implement traits easily. For example, |
| 5 | + |
| 6 | +```rust |
| 7 | +#[derive(Debug)] |
| 8 | +struct Point { |
| 9 | + x: i32, |
| 10 | + y: i32, |
| 11 | +} |
| 12 | +``` |
| 13 | + |
| 14 | +is a lot simpler than |
| 15 | + |
| 16 | +```rust |
| 17 | +struct Point { |
| 18 | + x: i32, |
| 19 | + y: i32, |
| 20 | +} |
| 21 | + |
| 22 | +use std::fmt; |
| 23 | + |
| 24 | +impl fmt::Debug for Point { |
| 25 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 26 | + write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y) |
| 27 | + } |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +Rust includes several traits that you can derive, but it also lets you define |
| 32 | +your own. We can accomplish this task through a feature of Rust called |
| 33 | +"procedural macros." Eventually, procedural macros will allow for all sorts of |
| 34 | +advanced metaprogramming in Rust, but today, they're only for custom derive. |
| 35 | + |
| 36 | +Let's build a very simple trait, and derive it with custom derive. |
| 37 | + |
| 38 | +## Hello World |
| 39 | + |
| 40 | +So the first thing we need to do is start a new crate for our project. |
| 41 | + |
| 42 | +```bash |
| 43 | +$ cargo new --bin hello-world |
| 44 | +``` |
| 45 | + |
| 46 | +All we want is to be able to call `hello_world()` on a derived type. Something |
| 47 | +like this: |
| 48 | + |
| 49 | +```rust,ignore |
| 50 | +#[derive(HelloWorld)] |
| 51 | +struct Pancakes; |
| 52 | +
|
| 53 | +fn main() { |
| 54 | + Pancakes::hello_world(); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +With some kind of nice output, like `Hello, World! My name is Pancakes.`. |
| 59 | + |
| 60 | +Let's go ahead and write up what we think our macro will look like from a user |
| 61 | +perspective. In `src/main.rs` we write: |
| 62 | + |
| 63 | +```rust,ignore |
| 64 | +#[macro_use] |
| 65 | +extern crate hello_world_derive; |
| 66 | +
|
| 67 | +trait HelloWorld { |
| 68 | + fn hello_world(); |
| 69 | +} |
| 70 | +
|
| 71 | +#[derive(HelloWorld)] |
| 72 | +struct FrenchToast; |
| 73 | +
|
| 74 | +#[derive(HelloWorld)] |
| 75 | +struct Waffles; |
| 76 | +
|
| 77 | +fn main() { |
| 78 | + FrenchToast::hello_world(); |
| 79 | + Waffles::hello_world(); |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Great. So now we just need to actually write the procedural macro. At the |
| 84 | +moment, procedural macros need to be in their own crate. Eventually, this |
| 85 | +restriction may be lifted, but for now, it's required. As such, there's a |
| 86 | +convention; for a crate named `foo`, a custom derive procedural macro is called |
| 87 | +`foo-derive`. Let's start a new crate called `hello-world-derive` inside our |
| 88 | +`hello-world` project. |
| 89 | + |
| 90 | +```bash |
| 91 | +$ cargo new hello-world-derive |
| 92 | +``` |
| 93 | + |
| 94 | +To make sure that our `hello-world` crate is able to find this new crate we've |
| 95 | +created, we'll add it to our toml: |
| 96 | + |
| 97 | +```toml |
| 98 | +[dependencies] |
| 99 | +hello-world-derive = { path = "hello-world-derive" } |
| 100 | +``` |
| 101 | + |
| 102 | +As for our the source of our `hello-world-derive` crate, here's an example: |
| 103 | + |
| 104 | +```rust,ignore |
| 105 | +extern crate proc_macro; |
| 106 | +extern crate syn; |
| 107 | +#[macro_use] |
| 108 | +extern crate quote; |
| 109 | +
|
| 110 | +use proc_macro::TokenStream; |
| 111 | +
|
| 112 | +#[proc_macro_derive(HelloWorld)] |
| 113 | +pub fn hello_world(input: TokenStream) -> TokenStream { |
| 114 | + // Construct a string representation of the type definition |
| 115 | + let s = input.to_string(); |
| 116 | + |
| 117 | + // Parse the string representation |
| 118 | + let ast = syn::parse_macro_input(&s).unwrap(); |
| 119 | +
|
| 120 | + // Build the impl |
| 121 | + let gen = impl_hello_world(&ast); |
| 122 | + |
| 123 | + // Return the generated impl |
| 124 | + gen.parse().unwrap() |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +So there is a lot going on here. We have introduced two new crates: [`syn`] and |
| 129 | +[`quote`]. As you may have noticed, `input: TokenSteam` is immediately converted |
| 130 | +to a `String`. This `String` is a string representation of the Rust code for which |
| 131 | +we are deriving `HelloWorld` for. At the moment, the only thing you can do with a |
| 132 | +`TokenStream` is convert it to a string. A richer API will exist in the future. |
| 133 | + |
| 134 | +So what we really need is to be able to _parse_ Rust code into something |
| 135 | +usable. This is where `syn` comes to play. `syn` is a crate for parsing Rust |
| 136 | +code. The other crate we've introduced is `quote`. It's essentially the dual of |
| 137 | +`syn` as it will make generating Rust code really easy. We could write this |
| 138 | +stuff on our own, but it's much simpler to use these libraries. Writing a full |
| 139 | +parser for Rust code is no simple task. |
| 140 | + |
| 141 | +[`syn`]: https://crates.io/crates/syn |
| 142 | +[`quote`]: https://crates.io/crates/quote |
| 143 | + |
| 144 | +The comments seem to give us a pretty good idea of our overall strategy. We |
| 145 | +are going to take a `String` of the Rust code for the type we are deriving, parse |
| 146 | +it using `syn`, construct the implementation of `hello_world` (using `quote`), |
| 147 | +then pass it back to Rust compiler. |
| 148 | + |
| 149 | +One last note: you'll see some `unwrap()`s there. If you want to provide an |
| 150 | +error for a procedural macro, then you should `panic!` with the error message. |
| 151 | +In this case, we're keeping it as simple as possible. |
| 152 | + |
| 153 | +Great, so let's write `impl_hello_world(&ast)`. |
| 154 | + |
| 155 | +```rust,ignore |
| 156 | +fn impl_hello_world(ast: &syn::MacroInput) -> quote::Tokens { |
| 157 | + let name = &ast.ident; |
| 158 | + quote! { |
| 159 | + impl HelloWorld for #name { |
| 160 | + fn hello_world() { |
| 161 | + println!("Hello, World! My name is {}", stringify!(#name)); |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +So this is where quotes comes in. The `ast` argument is a struct that gives us |
| 169 | +a representation of our type (which can be either a `struct` or an `enum`). |
| 170 | +Check out the [docs](https://docs.rs/syn/0.10.5/syn/struct.MacroInput.html), |
| 171 | +there is some useful information there. We are able to get the name of the |
| 172 | +type using `ast.ident`. The `quote!` macro let's us write up the Rust code |
| 173 | +that we wish to return and convert it into `Tokens`. `quote!` let's us use some |
| 174 | +really cool templating mechanics; we simply write `#name` and `quote!` will |
| 175 | +replace it with the variable named `name`. You can even do some repetition |
| 176 | +similar to regular macros work. You should check out the |
| 177 | +[docs](https://docs.rs/quote) for a good introduction. |
| 178 | + |
| 179 | +So I think that's it. Oh, well, we do need to add dependencies for `syn` and |
| 180 | +`quote` in the `cargo.toml` for `hello-world-derive`. |
| 181 | + |
| 182 | +```toml |
| 183 | +[dependencies] |
| 184 | +syn = "0.10.5" |
| 185 | +quote = "0.3.10" |
| 186 | +``` |
| 187 | + |
| 188 | +That should be it. Let's try to compile `hello-world`. |
| 189 | + |
| 190 | +```bash |
| 191 | +error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type |
| 192 | + --> hello-world-derive/src/lib.rs:8:3 |
| 193 | + | |
| 194 | +8 | #[proc_macro_derive(HelloWorld)] |
| 195 | + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 196 | +``` |
| 197 | + |
| 198 | +Oh, so it appears that we need to declare that our `hello-world-derive` crate is |
| 199 | +a `proc-macro` crate type. How do we do this? Like this: |
| 200 | + |
| 201 | +```toml |
| 202 | +[lib] |
| 203 | +proc-macro = true |
| 204 | +``` |
| 205 | + |
| 206 | +Ok so now, let's compile `hello-world`. Executing `cargo run` now yields: |
| 207 | + |
| 208 | +```bash |
| 209 | +Hello, World! My name is FrenchToast |
| 210 | +Hello, World! My name is Waffles |
| 211 | +``` |
| 212 | + |
| 213 | +We've done it! |
0 commit comments