8 Rust Macros Concept You Should Know

Master Rust's macro system for metaprogramming. This guide covers declarative vs procedural macros, hygiene, recursion, debugging tips, and key concepts for building robust macros.

Hey friends! Let's learn about procedural macros - one of Rust's most powerful (and confusing) features. Macros look like magic spells, but they're just code that writes code.

See, macros let you generate Rust code automatically during compilation. It's like having a robot assistant to write boring repetitive stuff for you. Pretty sweet!

For example, check out this derive macro:

#[derive(Serialize)] 
struct User {
  name: String,
}

The derive macro implements JSON serialization for User - so you don't have to! It just works its magic in the background.

Macros operate on tokens - the tiniest bits of Rust code like var names, symbols, keywords. They take tokens as input, do their thang, and output new tokens. Like this println macro:

let print_macro = |input: TokenStream| {
  let code = input.to_string();
  quote! {
    println!("{}", code);
  }
}

This turns tokens into a string and prints it. Macro magic!

There's two macro types:

  • Declarative: Annotate code or call functions. Easy syntax.
  • Procedural: Total control over code gen. Advanced skills.

Macros keep things hygienic using isolation chambers, so no sneaky bugs! They can also call themselves recursively like codegen dominos. Falling tokens trigger more tokens!

So that's macros in a nutshell! Let's dive in and start bending Rust to your will! This guide will walk through macros from the ground up - when we're done, you'll be a metaprogramming wizard! The magical world of macros awaits...

1. Declarative Macros: Annotate and Call

Declarative macros let you slap cool annotations on code or call macro functions. Their syntax looks just like regular Rust code - nothing too crazy.

For example, Rust has a declarative macro for automatically implementing traits:

#[derive(Serialize, Deserialize)]
struct User {
  name: String,
  age: u8
}

This derive macro automagically makes User serialize to JSON. No sweat! We just declare "User should be serializable!" and the macro handles the dirty work.

Here's another example using a regex macro:

let re = regex!(r"\d{3}-\d{3}-\d{4}");

regex! builds a regex from a string without all the boilerplate. We just call it like a function!

Declarative macros don't really modify your code under the hood. They just expand early before the rest of the magic happens. So you can sprinkle them on your code like spice without worrying too much.

2. Procedural Macros: Code Generation Magic

In Rust, procedural macros are a way to write code that generates other code during the compilation process. Think of them as code that writes code for you. They can be really powerful and save you a lot of time when you need to generate repetitive or boilerplate code.

Let's start with an example. Imagine you have a Rust program where you want to create a bunch of functions that add two numbers together. Instead of writing each function manually, you can use a procedural macro to generate them for you. Here's how you can do it:

// First, you need to import the `proc_macro` crate and the `TokenStream` type.
extern crate proc_macro;
use proc_macro::TokenStream;

// Define your procedural macro function.
#[proc_macro]
pub fn generate_add_functions(input: TokenStream) -> TokenStream {
    // Parse the input, which could be something like `generate_add_functions!(5, 10);`
    let input = input.to_string();
    let nums: Vec<u32> = input
        .split(',')
        .map(|s| s.trim().parse().unwrap())
        .collect();

    // Generate the code for adding the numbers.
    let mut output = String::new();
    for (i, num) in nums.iter().enumerate() {
        let function_name = format!("add_{}", i);
        output += &format!(
            "pub fn {}(x: u32) -> u32 {{\n    x + {}\n}}\n",
            function_name, num
        );
    }

    // Convert the generated code into a TokenStream and return it.
    output.parse().unwrap()
}

We import the proc_macro crate and the TokenStream type, which are necessary for creating procedural macros.

We define a procedural macro called generate_add_functions. The #[proc_macro] attribute tells Rust that this is a procedural macro.

Inside the macro, we parse the input, which is the numbers we want to add. For example, if we call generate_add_functions!(5, 10);, the input will be "5, 10".

We then generate code for each addition function based on the input numbers. For example, for input "5, 10", we generate a function called add_0 that adds 5 to the input and another function called add_1 that adds 10 to the input.

Finally, we convert the generated code into a TokenStream and return it.

Now, you can use this procedural macro in your Rust code like this:

use my_macros::generate_add_functions;

generate_add_functions!(5, 10);

fn main() {
    let result = add_0(7);
    println!("Result: {}", result); // This will print "Result: 12"
}

When you compile your Rust program, the procedural macro will generate the add_0 and add_1 functions for you, and you can use them just like any other Rust functions.

That's the basic idea of procedural macros in Rust. They allow you to automate code generation and make your code more concise and maintainable.

3. Macro Hygiene: Avoid Naming Clashes

Hygienic macros isolate the macro body in a new scope to avoid accidental name captures:

let x = "outer";

macro_rules! print_inner {
  () => {
    let x = "inner";
    println!("{}", x);
  };
}

print_inner!(); // Prints "inner"

Even though x is defined outside the macro, the inner x binding doesn't conflict due to hygiene.

macro_rules! math {
  ($x:ident) => {
    let $x = 2;
    $x + $x
  };  
}

let y = 1;
let z = math!(y); // z = 4, y unchanged

Here the macro introduces a new $x without changing the outer y. Hygiene prevents tricky name clash bugs.

Hygiene makes macros safer and more robust by isolating them in distinct expansion environments per invocation. This avoids accidental capture of identifiers.

4. Recursive Macro Expansion

Macros expand recursively, calling themselves or other macros until final Rust tokens are produced:

macro_rules! math {
  ($(+ $x:expr),*) => {{  
    let mut sum = 0;
    $(
      sum += $x;
    )*
    sum
  }};
}

let x = math!(+ 1, + 2, + 3);

math! recursively expands + 1, + 2, + 3 into the sum expression 1 + 2 + 3. This recursion enables repeating logic.

macro_rules! vector {
  ($elem:expr; $n:expr) => {
    vector!($elem; $n - 1) 
    ($elem, )
  };

  ($elem:expr; 0) => (());  
}

let v = vector!(1i32; 3); 

Here vector! recursively invokes itself to generate the vector elements. The motivation is avoiding repetitive element creation.

Recursion allows macros to call themselves or other macros, generating large amounts of code through repeated expansions.

5. Recursive Macros: Call Yourself

Macros can recursively call themselves to repeat logic:

macro_rules! factorial {
  ($x:expr) => {
    if $x == 0 {
      1  
    } else {
      $x * factorial!($x - 1)
    }
  };
}

println!("{}", factorial!(5)); // Prints 120

This factorial macro recursively calls itself to calculate factorials. The motivation is avoiding explicit factorial loops.

macro_rules! vector {
  ($elem:expr; $n:expr) => {
    {
      let mut v = Vec::new();
      for _ in 0..$n {
        v.push($elem);
      }
      v
    }
  };
}

let v = vector!(1i32; 10);

Here vector! recursively generates a vector of the given size. Recursion enables concise generation of repetitive code.

Recursive macros are a powerful technique for dynamically generating large amounts of code in a clear, concise way.

6. Syntax Extensions: Grow the Language

Macros can be used to extend Rust's syntax by adding new constructs that generate code:

macro_rules! route {
  ($method:ident $path:literal) => {
    #[route(method = $method, path = $path)]
    fn route() {
      // handler logic
    }
  }
}

route!(GET "/");
route!(POST "/login");

The route! macro lets you declare new routes using custom syntax. This avoids verbose function attributes.

macro_rules! thread_spawn {
  ($expr:expr) => {
    std::thread::spawn(move || {
      $expr
    })
  };
}

thread_spawn! {
  println!("Hello from new thread!");
}

thread_spawn! provides syntactic sugar for spawning a new thread and running an expression in it.

Syntax extensions allow designing domain-specific languages tailored for a problem space. Macros let you grow Rust's syntax to be more expressive.

7. Generate Repetitive Code

Macros can match on repeating syntax patterns, useful for generating repetitive code:

macro_rules! hashmap {
  { $($key:expr => $value:expr),+ } => {
    {
      let mut map = HashMap::new();
      $(
        map.insert($key, $value);
      )+
      map
    }
  };
}

let map = hashmap! {
  "a" => 1,
  "b" => 2 
};

The $(...)+ matcher allows cleanly defining a hashmap from multiple key-value pairs.

macro_rules! vectors {
  ($($elem:expr),*) => {
    {
      let mut v = Vec::new();
      $(
        v.push($elem);
      )*
      v
    }
  }
}

let v = vectors![1, 2, 3];

Here $(...)* builds a vector from a comma-separated list of elements.

Repetition patterns are useful for iterating over syntax to generate collections, structs, and more.

8. Macro Limits: Know the Boundaries

While Rust macros are extremely capable, they do have some boundaries you need to keep in mind:

  • Declarative macros can only be used as attributes or function-like calls. You can't use them to generate arbitrary code like procedural macros can.
// Invalid - declarative macros can't generate code  
macro_rules! my_macro {
  () => {
    fn do_something() {
      // ...
    }
  };
}
  • Procedural macros can generate code, but only valid Rust source code. They can't output raw text or anything that wouldn't compile.
// Invalid - can only return valid Rust tokens
proc_macro_derive(input: TokenStream) -> TokenStream {
  "Hello world!".parse().unwrap()
} 
  • Macros don't have access to runtime information besides pure constants. They generate code at compile time and can't read dynamic state.
// Invalid - can't access runtime variable
let x = 5;

macro_rules! print_x {
  () => {
    println!("x is {}", x);
  }
}

Knowing the boundaries of what macros can and can't do will prevent frustration. While powerful, macros aren't magic - keep their limits in mind!

Conclusion

We covered a lot of ground - declarative macros for annotating code, procedural macros for generating code, hygienic practices to avoid bugs, and even recursive wizardry! It's a lot, I know. But with time and compassion, you'll get there.

Soon these once-mystifying macros will feel like second nature. You'll wield meta0-programming might to vanquish repetition and banish verbosity! But for today, be proud of how far you've come. The macro arts are within your grasp. This is only the beginning.

Onward, friend - let's continue mastering Rust together! Our code is stronger when we cultivate community. So stay curious, stay humble, and most of all - have fun! The future looks bright. The magical world of Rust programming awaits. And with these new abilities, just think of all the great things you will build!

Subscribe to JS Dev Journal

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe