Declarative and Procedural Macros in Depth

Compile-Time Code Generation

Rust offers four complementary mechanisms for compile-time code generation: const evaluation (const fn / const {}), declarative macros (macro_rules!), procedural macros, and Cargo build scripts (build.rs + include!). Selecting the simplest mechanism that fits the problem keeps build times fast and error messages clear.

Comparing the Four Compile-Time Code Generation Mechanisms

Do you need to generate new AST items/types or only compute values?
  |
  +-- Compute constant values/tables of an existing type ---> `const fn` / `const { ... }`
  |
  +-- Generate repetitive Rust syntax / trait impls:
        +-- Pattern-based substitution on tokens ----------> `macro_rules!`
        +-- Inspecting struct/enum fields or custom DSL ---> Procedural Macro (`#[derive]`, `#[proc_macro]`)
        +-- Reading external schemas (.proto, C headers, SQLite schema, git rev)
              +--------------------------------------------> `build.rs` + `include!(concat!(env!("OUT_DIR"), ...))`
Mechanism Can Read External Files / Env? Can Generate New Types / Impls? IDE (rust-analyzer) Support Compile-Time Cost
const fn / const {} Only via include_bytes! / env! No (values only) Full (normal type-checked Rust) Lowest
macro_rules! No Yes High (built-in expander) Very low
Procedural Macro Sandboxed (should not perform I/O!) Yes (inspects AST) High (via proc-macro server) Moderate (compiles syn + host dylib)
build.rs + include! Yes (cargo:rerun-if-changed=...) Yes (writes .rs file to OUT_DIR) Good when cargo check runs build.rs Moderate (runs build binary before crate)

Generating Static Code Tables with macro_rules!

When generating both an enum, its string parser, and its metadata table from a single source of truth so they can never drift out of sync, a single declarative macro generates all three items at once:

macro_rules! define_http_methods {
    ( $( $Variant:ident => $wire_str:literal (idempotent = $idem:literal) ),+ $(,)? ) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        pub enum HttpMethod {
            $( $Variant ),+
        }

        impl HttpMethod {
            pub const ALL: &'static [Self] = &[ $( Self::$Variant ),+ ];

            pub const fn as_str(self) -> &'static str {
                match self {
                    $( Self::$Variant => $wire_str ),+
                }
            }

            pub const fn is_idempotent(self) -> bool {
                match self {
                    $( Self::$Variant => $idem ),+
                }
            }

            pub fn from_str(s: &str) -> Option<Self> {
                match s {
                    $( $wire_str => Some(Self::$Variant), )+
                    _ => None,
                }
            }
        }
    };
}

define_http_methods! {
    Get    => "GET"    (idempotent = true),
    Post   => "POST"   (idempotent = false),
    Put    => "PUT"    (idempotent = true),
    Delete => "DELETE" (idempotent = true),
}

fn main() {
    for &method in HttpMethod::ALL {
        println!(
            "{:<6} -> idempotent = {:<5} | roundtrip = {:?}",
            method.as_str(),
            method.is_idempotent(),
            HttpMethod::from_str(method.as_str())
        );
    }
}
GET    -> idempotent = true  | roundtrip = Some(Get)
POST   -> idempotent = false | roundtrip = Some(Post)
PUT    -> idempotent = true  | roundtrip = Some(Put)
DELETE -> idempotent = true  | roundtrip = Some(Delete)

Why Procedural Macros Must Not Perform Arbitrary File or Network I/O

Unlike build.rs (which emits cargo:rerun-if-changed=path directives so Cargo knows when to re-run the build script), procedural macros have no stable dependency-tracking protocol for external files or network calls. If a procedural macro reads an external file directly using std::fs::read_to_string, Cargo and rust-analyzer will not know to re-expand the macro when that file changes, leading to stale incremental builds. If a macro needs file contents, pass include_str!("path") into the macro so the compiler tracks the file dependency automatically!