Project 10: Procedural Macro Library (Compile-Time Code Generation)

A derive macro that auto-generates code at compile time—like implementing #[derive(Builder)] that creates builder patterns, or #[derive(Serialize)] that creates serialization code.

Quick Reference

Attribute Value
Primary Language Rust
Alternative Languages None (Rust-specific feature)
Difficulty Level 4: Expert
Time Estimate 2-3 weeks
Knowledge Area Metaprogramming / Macros / Compiler Integration
Tooling syn, quote, proc-macro2
Prerequisites Most prior projects completed, strong Rust fundamentals

What You Will Build

A derive macro that auto-generates code at compile time—like implementing #[derive(Builder)] that creates builder patterns, or #[derive(Serialize)] that creates serialization code.

Why It Matters

This project builds core skills that appear repeatedly in real-world systems and tooling.

Core Challenges

  • Parsing Rust syntax with syn → maps to understanding TokenStream and AST
  • Generating valid Rust code with quote → maps to code generation
  • Handling edge cases and error reporting → maps to compile-time diagnostics
  • Testing macro output → maps to macro debugging techniques

Key Concepts

  • Procedural macros: “The Rust Programming Language” Chapter 19 - Steve Klabnik
  • TokenStream manipulation: “Rust for Rustaceans” Chapter 9 - Jon Gjengset
  • syn crate for parsing: syn documentation
  • quote crate for generation: quote documentation

Real-World Outcome

// User writes this:
#[derive(Builder)]
struct Config {
    host: String,
    port: u16,
    #[builder(default = "false")]
    debug: bool,
    #[builder(optional)]
    timeout: Option<u64>,
}

// Your macro generates this at compile time:
impl Config {
    fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }
}

struct ConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
    debug: bool,
    timeout: Option<u64>,
}

impl ConfigBuilder {
    fn host(mut self, value: String) -> Self {
        self.host = Some(value);
        self
    }

    fn port(mut self, value: u16) -> Self {
        self.port = Some(value);
        self
    }

    fn debug(mut self, value: bool) -> Self {
        self.debug = value;
        self
    }

    fn timeout(mut self, value: u64) -> Self {
        self.timeout = Some(value);
        self
    }

    fn build(self) -> Result<Config, &'static str> {
        Ok(Config {
            host: self.host.ok_or("host is required")?,
            port: self.port.ok_or("port is required")?,
            debug: self.debug,
            timeout: self.timeout,
        })
    }
}

// Usage:
fn main() {
    let config = Config::builder()
        .host("localhost".into())
        .port(8080)
        .build()
        .unwrap();
}

Implementation Guide

  1. Reproduce the simplest happy-path scenario.
  2. Build the smallest working version of the core feature.
  3. Add input validation and error handling.
  4. Add instrumentation/logging to confirm behavior.
  5. Refactor into clean modules with tests.

Milestones

  • Milestone 1: Minimal working program that runs end-to-end.
  • Milestone 2: Correct outputs for typical inputs.
  • Milestone 3: Robust handling of edge cases.
  • Milestone 4: Clean structure and documented usage.

Validation Checklist

  • Output matches the real-world outcome example
  • Handles invalid inputs safely
  • Provides clear errors and exit codes
  • Repeatable results across runs

References

  • Main guide: LEARN_RUST_DEEP_DIVE.md
  • “The Rust Programming Language” by Steve Klabnik (Chapter 19) + “Rust for Rustaceans”