Macros
What can macros do?
Macros can be used to things such as:
- Generate repetitive code
- Create Domain-Specific Languages (or DSLs)
- Write things that would otherwise be hard without Macros
There are two kinds of macro
- Declarative
- Procedural
Declarative Macros
Declarative Macros
- Defined using
macro_rules!
- Perform pattern matching and substitution
- Can do repeated actions
Declarative Macros are:
- Hygienic: expansion happens in a different 'syntax context'
- Correct: they cannot expand to invalid code
- Limited: they cannot, for example, pollute their expansion site
The vec!
macro
fn main() { // You write: let v = vec![1, 2, 3]; // The compiler sees (roughly): let v = { let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec }; }
How does that work?
"Match zero or more expressions, and paste each into into a temp_vec.push()
call"
#![allow(unused)] fn main() { #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } }
Note:
The actual macro is more complicated as it sets the Vec
to have the correct capacity up front, to avoid re-allocation during the pushing of the values. Any new variables we introduce are given a colour to distinguish them from any the caller had created in the same scope.
println!
and friends
println!
is a macro, because:
- Rust does not have variadic functions
- Rust wants to type-check the call
Expanding println!
fn main() {
// You write
println!("Hello {}, aged {}", "Sam", 40);
// The compiler sees (roughly):
let arguments = Arguments {
pieces: &["Hello ", ", aged ", "\n"],
args: &[
Argument { value: &"Sam", formatter: string_formatter },
Argument { value: &40, formatter: integer_formatter },
],
};
::std::io::_print(arguments);
}
Note:
This is a simplified example - the real output is slightly more complicated, and is in fact handled by a compiler built-in so you can't even see the macro source for yourself.
Downsides of Declarative Macros
- Can be difficult to debug
- Can be confusing to read and understand
When Should You Use Declarative Macros?
- When there are no other good alternatives
Procedural macros
Procedural macros
- A procedural macro is a function that takes some code as input, and produces some code.
- It runs at compile time
- It is written in Rust and must therefore be compiled before your program is
Three kinds of procedural macro
- Custom
#[derive]
macros - Attribute-like macros
- Function-like macros
Custom #[derive]
macros
Work like the built-in Rust derives, once you've imported them:
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
struct Square {
width: u32,
}
fn main() {
let sq = Square { width: 25 };
let json = serde_json::to_string(&sq).unwrap();
println!("{}", json);
}
Often named after the traits they implement.
Note:
In the Rust Docs search results, the trait appears in blue, and the macro appears in green.
Rust can always work out whether you mean the trait or the macro, from the context.
Attribute-like macros
- Placed above a type, function, or field
- Can have optional arguments
#[tokio::main(worker_threads = 2)]
async fn main() {
println!("Hello world");
}
Function-like macros
Called like a function:
let query = sqlx::query!("SELECT * FROM `person`");
Downsides of Procedural Macros
- Can be difficult to debug
- Slows down compilation a lot
- Have to be stored in a separate crate
- You're basically building compiler plug-ins at build time
When Should You Use Procedural Macros?
- When it saves your users a sufficient amount of work