Skip to main content

One post tagged with "macro"

View All Tags

Rust Macros

· 4 min read
forfd8960
Author

What is a Rust Macro?

  • Core Concept: Macros are a way of writing code that writes other code, a form of metaprogramming.
  • Compile-Time Expansion: Unlike functions (which are called at runtime), macros are expanded during compilation. The Rust compiler replaces a macro invocation with the code generated by the macro before compiling the rest of the code.

Purpose

  • Reduce Boilerplate: Automate repetitive code patterns (DRY - Don't Repeat Yourself). The println!, vec!, and assert! macros are common examples.

  • Domain-Specific Languages (DSLs): Create syntax tailored for a specific problem domain within Rust code.

  • Variadic Functions: Implement functions that can take a variable number of arguments (like println!). Rust functions must have a fixed arity.

  • Extend Language Syntax: Introduce new syntactic constructs that aren't built directly into the core language.

How Many Types of Macros?

Rust fundamentally has two major types of macro systems:

  • a) Declarative Macros (macro_rules!)
  • b) Procedural Macros (which come in three kinds)

How to Apply Them Into Code (Details & Examples)

a) Declarative Macros (macro_rules!)

How They Work: Defined using the macro_rules! macro itself. They work by matching against patterns in the Rust code structure and replacing the matched code with the specified output template. They are similar in concept to match expressions but operate on code structure (token trees) instead of values.

macro_rules! macro_name {
// Rule 1: Match a specific pattern => Generate specific code
( $(pattern_matcher) ) => { $(code_template) };

// Rule 2: Match another pattern => Generate different code
( $(another_pattern) ) => { $(another_template) };

// ... more rules
}

Called using the macro name followed by ! and parentheses (), brackets [], or braces containing the arguments. Example: macro_name!(...), macro_name!{...}, macro_name![...].

Simple Example: A macro to create a simple HashMap

use std::collections::HashMap;

macro_rules! create_map {
// Match key-value pairs separated by commas
( $( $key:expr => $value:expr ),* $(,)? ) => {
{ // Use a block to create a scope
let mut map = HashMap::new();
$( // Repeat for each matched pair
map.insert($key, $value);
)*
map // Return the map
}
};
}

fn main() {
let my_map = create_map! {
"one" => 1,
"two" => 2,
"three" => 3
};
println!("{:?}", my_map); // Output: {"one": 1, "two": 2, "three": 3} (order may vary)
}

b) Procedural Macros (Proc Macros)

How They Work

These are more complex than declarative macros. They are essentially regular Rust functions that operate on the stream of tokens representing the input code and produce a new stream of tokens as output. They offer much more flexibility but are also harder to write

Requirement

Procedural macros must be defined in their own separate crate with a specific crate type declared in its Cargo.toml:

[lib]
proc-macro = true

Three Kinds of Procedural Macros

Custom #[derive] Macros

Allow you to automatically implement traits for structs and enums by adding #[derive(YourCustomTrait)] above the type definition.

A function marked with #[proc_macro_derive(TraitName)] that takes a TokenStream (representing the struct/enum definition) and returns a TokenStream (representing the generated impl block).

#[derive(YourDeriveMacroName)] above a struct or enum. Conceptual Example (Code is complex, showing usage):

// Assuming HelloDerive is defined in another proc-macro crate
use your_macros::HelloDerive;

#[derive(HelloDerive)] // Apply the custom derive
struct MyStruct {
field: i32,
}

fn main() {
// The derive macro might generate something like this:
// impl MyStruct {
// fn say_hello(&self) { println!("Hello from MyStruct!"); }
// }
let s = MyStruct { field: 10 };
// s.say_hello(); // This method would exist if generated
}

Attribute-like Macros

// Assuming route is defined in another proc-macro crate
use your_web_framework::route;

#[route(GET, "/users")] // Apply the attribute macro
fn list_users() {
// ... handler logic ...
}

// The macro might transform this into code that registers
// list_users function with a router for GET requests to "/users".

Function-like Macros

// Assuming sql is defined in another proc-macro crate
use your_db_macros::sql;

fn main() {
let user_id = 123;
// The macro could parse the SQL, check syntax at compile time,
// and generate code to execute it safely with parameters.
let user_name: String = sql!("SELECT name FROM users WHERE id = ?", user_id);
println!("User name: {}", user_name);
}