cargo-expand: See What Rust Macros Actually Generate
cargo-expand: See What Rust Macros Actually Generate
Debugging Rust macros is painful. Compiler errors point to the macro invocation, not what it generates. cargo-expand fixes that.
The Problem
1
2
3
4
5
6
7
8
#[derive(Debug, Serialize)]
struct User {
id: u32,
name: String,
}
// Error: trait bound `User: Serialize` is not satisfied
// But what code did #[derive(Serialize)] actually generate?
No idea what the macro produced.
Install cargo-expand
1
2
3
4
5
6
7
8
# Need nightly toolchain (doesn't have to be default)
rustup toolchain install nightly
# Install
cargo install cargo-expand
# Use
cargo expand
Usage
1
2
3
4
5
6
7
8
# Expand everything
cargo expand
# Expand specific item
cargo expand MyStruct
# Expand module
cargo expand my_module
Example: What Does #[derive(Debug)] Generate?
1
2
3
4
5
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
Run cargo expand:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Expanded code:
impl ::core::fmt::Debug for Point {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(
f,
"Point",
"x",
&&self.x,
"y",
&&self.y,
)
}
}
Now I can see exactly what Debug does!
Useful for Understanding
1. Derive Macros
See how #[derive(Clone, Serialize, etc)] are implemented.
2. Procedural Macros
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[tokio::main]
async fn main() {
// ...
}
// Expands to:
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
// your code
})
}
Aha! That’s what #[tokio::main] does.
3. Declarative Macros
1
2
3
4
5
6
7
macro_rules! vec_of_strings {
($($x:expr),*) => {
vec![$($x.to_string()),*]
};
}
let v = vec_of_strings!["hello", "world"];
cargo expand shows the final expanded vec! and .to_string() calls.
Pretty Output
1
2
3
4
5
6
7
8
# Colorized
cargo expand | bat -l rust
# Save to file for comparison
cargo expand > before.rs
# make changes
cargo expand > after.rs
diff before.rs after.rs
When I Use It
- Debugging macro errors
- Learning how libraries use macros
- Understanding what
async-trait,thiserror, etc. actually do - Writing my own macros
One Gotcha
Needs nightly toolchain installed. Not a big deal:
1
2
3
rustup toolchain install nightly
# Don't need to set as default
cargo expand # automatically uses nightly
That’s it. Super useful when macros behave unexpectedly or you want to understand what they’re doing under the hood.
This post is licensed under CC BY 4.0 by the author.