]>
Commit | Line | Data |
---|---|---|
8bb4bdeb | 1 | # Procedural Macros (and custom Derive) |
476ff2be SL |
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 | ||
8bb4bdeb | 102 | As for the source of our `hello-world-derive` crate, here's an example: |
476ff2be SL |
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 | |
cc61c64b | 118 | let ast = syn::parse_derive_input(&s).unwrap(); |
476ff2be SL |
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 | |
8bb4bdeb | 131 | we are deriving `HelloWorld`. At the moment, the only thing you can do with a |
476ff2be SL |
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 | |
7cac9316 | 156 | fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens { |
476ff2be SL |
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`). | |
7cac9316 | 170 | Check out the [docs](https://docs.rs/syn/0.11.11/syn/struct.DeriveInput.html), |
476ff2be | 171 | there is some useful information there. We are able to get the name of the |
8bb4bdeb XL |
172 | type using `ast.ident`. The `quote!` macro lets us write up the Rust code |
173 | that we wish to return and convert it into `Tokens`. `quote!` lets us use some | |
476ff2be SL |
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] | |
7cac9316 XL |
184 | syn = "0.11.11" |
185 | quote = "0.3.15" | |
476ff2be SL |
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! | |
8bb4bdeb XL |
214 | |
215 | ## Custom Attributes | |
216 | ||
217 | In some cases it might make sense to allow users some kind of configuration. | |
218 | For example, the user might want to overwrite the name that is printed in the `hello_world()` method. | |
219 | ||
220 | This can be achieved with custom attributes: | |
221 | ||
222 | ```rust,ignore | |
223 | #[derive(HelloWorld)] | |
224 | #[HelloWorldName = "the best Pancakes"] | |
225 | struct Pancakes; | |
226 | ||
227 | fn main() { | |
228 | Pancakes::hello_world(); | |
229 | } | |
230 | ``` | |
231 | ||
232 | If we try to compile this though, the compiler will respond with an error: | |
233 | ||
234 | ```bash | |
235 | error: The attribute `HelloWorldName` is currently unknown to the compiler and may have meaning added to it in the future (see issue #29642) | |
236 | ``` | |
237 | ||
238 | The compiler needs to know that we're handling this attribute and to not respond with an error. | |
239 | This is done in the `hello-world-derive` crate by adding `attributes` to the `proc_macro_derive` attribute: | |
240 | ||
241 | ```rust,ignore | |
242 | #[proc_macro_derive(HelloWorld, attributes(HelloWorldName))] | |
243 | pub fn hello_world(input: TokenStream) -> TokenStream | |
244 | ``` | |
245 | ||
246 | Multiple attributes can be specified that way. | |
247 | ||
248 | ## Raising Errors | |
249 | ||
250 | Let's assume that we do not want to accept enums as input to our custom derive method. | |
251 | ||
252 | This condition can be easily checked with the help of `syn`. | |
253 | But how do we tell the user, that we do not accept enums? | |
254 | The idiomatic way to report errors in procedural macros is to panic: | |
255 | ||
256 | ```rust,ignore | |
7cac9316 | 257 | fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens { |
8bb4bdeb XL |
258 | let name = &ast.ident; |
259 | // Check if derive(HelloWorld) was specified for a struct | |
260 | if let syn::Body::Struct(_) = ast.body { | |
261 | // Yes, this is a struct | |
262 | quote! { | |
263 | impl HelloWorld for #name { | |
264 | fn hello_world() { | |
265 | println!("Hello, World! My name is {}", stringify!(#name)); | |
266 | } | |
267 | } | |
268 | } | |
269 | } else { | |
270 | //Nope. This is an Enum. We cannot handle these! | |
271 | panic!("#[derive(HelloWorld)] is only defined for structs, not for enums!"); | |
272 | } | |
273 | } | |
274 | ``` | |
275 | ||
276 | If a user now tries to derive `HelloWorld` from an enum they will be greeted with following, hopefully helpful, error: | |
277 | ||
278 | ```bash | |
279 | error: custom derive attribute panicked | |
280 | --> src/main.rs | |
281 | | | |
282 | | #[derive(HelloWorld)] | |
283 | | ^^^^^^^^^^ | |
284 | | | |
285 | = help: message: #[derive(HelloWorld)] is only defined for structs, not for enums! | |
286 | ``` |