In Rust, macros allow for code generation at compile time, providing powerful tools for metaprogramming, reducing boilerplate, and implementing DSLs. The main types of macros:
macro_rules!.#[derive], attribute (#[some_macro]), and function-like (custom_macro!()).Declarative macros are simpler to use for template code, while procedural macros provide more control over syntax and token analysis.
Example of a declarative macro:
macro_rules! vec_of_strings { ($($x:expr),*) => { { let mut v = Vec::new(); $(v.push($x.to_string());)* v } }; } let v = vec_of_strings!("a", "b"); // => Vec<String>
Example of a procedural macro (derive):
#[derive(Debug, Clone)] struct MyStruct; // the derive Debug is implemented as a procedural macro in std.
Can Rust macros generate syntactically incorrect code or code with runtime errors?
Answer: Yes, macros do not check the correctness of expansion at the time of writing; errors may only show up after the macro substitution by the compiler. Declarative macros can lead to subtle syntactical errors. Procedural macros may generate incorrect or vulnerable code, so it is critically important to thoroughly test their behavior.
Example:
macro_rules! make_error { () => { let x = ; // syntax error will occur when using the macro } }
Story
In a large project to reduce boilerplate, macro_rules! was used without covering all pattern cases. A user mistakenly passed an unsupported expression to the macro, leading to an unclear compilation error whose cause was difficult to trace.
Story
When transferring procedural macros between crates, version incompatibility issues with the TokenStream API arose, causing the IDE to hang, and the error only manifested in no_std builds.
Story
When writing a DSL for configs with a procedural macro, unsafe parsing of input tokens was implemented (without type validation), leading to strange bugs, vulnerabilities, and the inability to correctly deploy part of the new functionality.