ProgrammingRust application/library architect

What are macros in Rust? Types of macros, the difference between procedural and declarative, when is it better to choose one over the other, and what dangers can arise from their use?

Pass interviews with Hintsage AI assistant

Answer

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:

  • Declarative macros (macro_rules!): defined using a match-like syntax, work on a template->replacement principle. The most typical form is macro_rules!.
  • Procedural macros: declared as external functions in a crate, receive an AST (TokenStream) and return modified code. They are divided into #[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.

Trick question

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

Examples of real errors due to lack of understanding of the topic


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.