cargo-expand: See What Rust Macros Actually Generate

Waken 2022-10-08

Debugging Rust macros is painful. Compiler errors point to the macro invocation, not what it generates. cargo-expand fixes that.

The Problem

#[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

# Need nightly toolchain (doesn't have to be default)
rustup toolchain install nightly

# Install
cargo install cargo-expand

# Use
cargo expand

Usage

# Expand everything
cargo expand

# Expand specific item
cargo expand MyStruct

# Expand module
cargo expand my_module

Example: What Does #[derive(Debug)] Generate?

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

Run cargo expand:

// 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

#[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

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

# 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:

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.