tezvyn:

Rust Procedural Macros: Code That Writes Code

AI-drafted, machine-checkedSource: doc.rust-lang.orgadvanced

Procedural macros are compile-time functions that write Rust code for you. They power common patterns like Serde's `#[derive(Serialize)]`. The main footgun is hygiene: generated code can clash with local variables, so authors must use absolute paths to be…

WHY IT EXISTS Procedural macros exist to extend Rust's syntax and eliminate repetitive boilerplate code. They allow library authors to create powerful abstractions and domain-specific languages (DSLs) that feel like native language features, saving users from writing complex, error-prone code manually.

THE MENTAL MODEL A procedural macro is a function that the compiler runs during compilation. It takes a stream of Rust code tokens as its input and produces a new stream of tokens as its output. This output is then inserted back into your program and compiled. Think of it as a pre-processor that operates on a structured representation of your code, not just text.

HOW IT WORKS Macros are defined in a special crate with proc-macro = true in its Cargo.toml. This crate is a dependency, not part of your main application code. The macro itself is a public function that accepts a TokenStream and returns a TokenStream. The compiler passes the relevant code—the struct for a derive macro, the function for an attribute macro—to this function as a stream of tokens. The macro then manipulates these tokens and returns a new stream for the compiler to substitute in. Because they run during compilation, they have the same permissions as the compiler, including filesystem access, which is a security consideration.

WHEN TO USE IT Use procedural macros for heavy-duty code generation. They come in three flavors. First, derive macros, like #[derive(Serialize)], which automatically implement traits for structs. Second, attribute macros, like #[tokio::main], which can transform an entire function. Third, function-like macros, which operate like a function call but with custom syntax inside, such as my_dsl!(...).

WHEN NOT TO USE IT Avoid procedural macros for simple substitutions; a declarative macro (macro_rules!) is cheaper and safer. The primary reason to be cautious is their unhygienic nature. The code they generate is injected directly into the surrounding scope, making it vulnerable to name collisions with user code. This complexity, plus the increased compile times, means they should be used only when the abstraction they provide is worth the cost.

ONE CANONICAL EXAMPLE A function-like macro can generate an entire function from a simple invocation. A macro defined with #[proc_macro] pub fn make_answer(_item: TokenStream) -> TokenStream can be implemented to return the string "fn answer() -> u32 { 42 }" parsed into a TokenStream. In another crate, a developer can simply write make_answer!();. During compilation, this line is replaced by the full answer function definition, making it callable within their code.

Read the original → doc.rust-lang.org

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.