C.W.K.
Stream
Lesson 04 of 04 · published

Macros in the Wild

~10 min · macros, serde, ecosystem

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

You now understand both macro kinds. This lesson grounds them in the macros you'll actually meet — the ones that quietly run your Rust programs.

The declarative macros you use daily

println!, format!, vec!, assert!, assert_eq!, panic! — all macro_rules! macros from the standard library. They exist as macros (not functions) precisely because they need variadic arguments or compile-time format checking. Every time you typed a ! in this quest, you invoked one.

The procedural macros that power the ecosystem

#[derive(Serialize, Deserialize)] from serde generates an entire serialization layer from your struct definition — arguably the most-used procedural macro in Rust. #[tokio::main] bootstraps the async runtime. #[derive(thiserror::Error)] generated your error boilerplate back in the Error Handling track. These macros are why Rust feels high-level despite being a systems language.

Macros are why Rust has so little boilerplate for a systems language. Serialization, error types, async setup, derives — all generated at compile time from a one-line annotation. You write the intent; the macro writes the mechanism. That's the leverage that makes Rust productive, not just safe and fast.

The takeaway

You don't need to write macros to be a productive Rust programmer — you need to read them: recognize what #[derive(...)] generates, know that a ! means compile-time expansion, and trust the well-tested ecosystem macros. Write your own only when boilerplate genuinely can't be solved by a function or generic. That judgment — function first, macro last — is the mark of mature Rust.

Code

serde: a one-line annotation generates serialization·rust
// Cargo.toml:  serde = { version = "1", features = ["derive"] }
//              serde_json = "1"
use serde::{Serialize, Deserialize};

// One annotation generates a full serialization implementation:
#[derive(Serialize, Deserialize, Debug)]
struct Config {
    name: String,
    port: u16,
}

fn main() {
    let c = Config { name: "app".into(), port: 8080 };
    // serde_json uses the generated impl to turn the struct into JSON:
    let json = serde_json::to_string(&c).unwrap();
    println!("{json}"); // {"name":"app","port":8080}
}

External links

Exercise

Add serde to a project, derive Serialize and Deserialize on a struct, and round-trip it through JSON with serde_json: struct -> string -> struct. Then reflect: how many lines of parsing/formatting code did the two derive macros save you, and what would maintaining that by hand have cost?
Hint
Hand-written serialization for even a small struct is tedious and bug-prone, and grows with every field. The two derives generate and maintain it automatically — add a field and the macros regenerate the code on the next build. That's the leverage.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.