A Rust macro is compile-time metaprogramming: it takes code as input and expands into ordinary Rust code such as structs, functions, or implementations. The generated result then proceeds through linting, type checking, and compilation like other code. Macros therefore add capabilities that functions alone cannot provide, while keeping generated code subject to Rust’s compiler checks. section 1.2 section 1.3
Macros are already part of everyday Rust. Invocation macros include format!, dbg!, and assert_eq!; attributes include #[test]; and derive attributes such as #[derive(Deserialize)] generate implementations from an annotated type. Rust provides declarative macros plus derive, attribute, and function-like procedural macros; procedural forms process code as token streams. section 1.1 section 1.6
Listing 1.1 The program you just wrote
use serde::Deserialize;
#[derive(Deserialize)] #1
struct Request {
given_name: String,
last_name: String,
}
fn full_name(given: &str, last: &str) -> String {
format!("{} {}", given, last) #2
}
fn main() {
let r = Request {
given_name: "Sam".to_string(),
last_name: "Hall".to_string()
};
dbg!(full_name(&r.given_name, &r.last_name));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize() {
let actual: Request =
serde_json::from_str("{ \"given_name\\": \\"Test\\",
\"last_name\\": \\"McTest\\" }")
.expect("deserialize to work");
assert_eq!(actual.given_name, "Test".to_string());
assert_eq!(actual.last_name, "McTest".to_string());
}
}
This small JSON-oriented program shows several macro forms in one place: a derive attribute, format!, dbg!, a test attribute, and assert_eq!. section 1.1
Expansion replaces only the macro invocation at its call site. A declarative macro can match an empty invocation such as my_vec!() and transcribe Vec::new(), leaving surrounding syntax, including the caller’s semicolon, intact. This expansion happens before checking, linting, or running the application. section 2.1.3
Reach for a custom macro when it provides a clear advantage:
- Remove substantial boilerplate or duplication when ordinary Rust would become unwieldy. section 1.3.2
- Create a domain-specific language when a complex domain benefits from more focused notation. section 1.3.3
- Generate repetitive APIs, such as builder methods, from a struct definition or its attributes. section 6.4.1 section 8.0
Do not make macros the default abstraction. Start with structs, enums, functions, generic functions, or generic implementations when they solve the problem clearly. Macro signatures reveal less than function signatures, can make code harder for unfamiliar readers to follow, add compile-time and binary-size costs, and receive less capable IDE support. Business logic is usually too specific to benefit from shared macros, although rare boilerplate and DSLs can be exceptions. section 1.3.2 section 1.3.3
Test generated builder behavior
A black-box test can define a struct with #[derive(Builder)] and assert that ExampleStructNoFields::builder().build() compiles and returns that struct. When generation fails, compiler messages may point at the macro rather than a precise generated line; cargo expand helps inspect the expanded test code. section 6.4.1
Production-oriented macros need more than a successful expansion. Preserve relevant attributes when transforming items, account for supported input shapes and generics, provide useful error handling, and test the promised effect at the appropriate level. A macro that drives external infrastructure may require end-to-end verification rather than unit tests alone. Libraries intended for others also need documentation, publishing preparation, and optional functionality through features. section 4.5 section 9.8 section 10.0








