Making Macros in Rust

And here procedural macros already have more capabilities, they allow you to manipulate the abstract syntax tree of the code, that is, you can analyze, modify and generate code on the fly. Procedural macros can be divided into three categories: attribute macros, derivative macros And function macros.

In this article we will look at how they are written in Rust.

Let's start with the declarative ones!

Declarative macros

So, declarative macros in Rust allow you to create code similar to the expression match in Rust, where the value is compared against patterns and the code associated with the corresponding pattern is executed. This happens at compile time. To define a macro, use the construct macro_rules!. For example, macro vec! allows you to create a new vector with the specified values:

Macro Definition Example vec!:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

A macro can take any number of arguments of any type and generate code to create a vector containing the specified elements. Macro body structure vec! similar to expression structure match. Here we see one option with a template ( $( $x:expr ),* )followed by a block of code associated with that template. If the pattern matches, the associated code block will be generated.

Let's look at the macro power!which can calculate the square or cube of a number:

macro_rules! power {
    ($value:expr, squared) => { $value.pow(2_i32) };
    ($value:expr, cubed) => { $value.pow(3_i32) };
}

This is where multiple matching is performed, allowing the macro to capture different levels of specificity. It is important to order the matching rules correctly to avoid incorrect matching!

Macros can accept a variable amount of input. For example, call vec![2] or vec![1, 2, 3] uses repetition operators similar to Regex. To add n numbers, you can use the following construction:

macro_rules! adder {
    ($($right:expr),+) => {{
    let mut total: i32 = 0;
    $( 
        total += $right;
    )+
    total
}};

In this case, we sum up all the passed values. + after a code fragment indicates that this fragment can be repeated one or more times.

Macros may require separators between repeating elements:

macro_rules! no_trailing {
    ($($e:expr),*) => {}
};

macro_rules! with_trailing {
    ($($e:expr,)*) => {}
};

macro_rules! either {
    ($($e:expr),* $(,)*) => {}
};

To perform several actions inside a macro, as you probably already noticed, double curly braces are used:

macro_rules! etwas {
    ($value:expr, squared) => {{ 
        let x: u32 = $value;
        x.pow(2)
    }}
};

You can process multiple sets of repeated data by using context to determine the number of repetitions for each data set:

macro_rules! operations {
    (add $($addend:expr),+; mult $($multiplier:expr),+) => {{
        let mut sum = 0;
        $(
            sum += $addend;
         )*

         let mut product = 1;
         $(
              product *= $multiplier;
          )*

          println!("Sum: {} | Product: {}", sum, product);
    }} 
};

Procedural macros

Procedural macros in Rust must be defined in separate crates with type proc-macro V Cargo.toml file. Procedural macros are divided into three main types: functional, user derived, and attribute.

To work with procedural macros, you will need to create a separate crate with the type proc-macro V Cargo.toml file:

[lib]
proc-macro = true

And add dependencies syn And quote for parsing incoming messages TokenStream and generating output code.

Functional macros

Function macros in Rust allow you to create language extensions that can take Rust code as input and generate Rust code as output. They are similar to functions in that they are called using the operator ! and look like function calls. Example of a simple function macro:

extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn my_fn_like_proc_macro(input: TokenStream) -> TokenStream {
    // логика обработки входного TokenStream
    // и генерации нового TokenStream.
    input
}

Macro my_fn_like_proc_macro accepts TokenStream as input (which is the code passed to the macro) and returns TokenStream as the output (which is the Rust code generated by the macro).

Let's say we want to create a macro that reads the name of a variable and returns a string with that name:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident};

#[proc_macro]
pub fn var_name(input: TokenStream) -> TokenStream {
    let input_parsed = parse_macro_input!(input as Ident);
    
    let name = input_parsed.to_string();
    
    let expanded = quote! {
        {
            let my_var_name = stringify!(#input_parsed);
            println!("Переменная: {}, значение: {}", #name, my_var_name);
        }
    };

    TokenStream::from(expanded)
}

Using crates syn for parsing macro input data into AST and quote to generate Rust code based on this AST. Macro var_name takes the name of a variable and generates code that prints the name of that variable and its value.

To use this macro, you need to write in code:

let my_variable = 42;
var_name!(my_variable);

This will call the macro var_namewhich will generate code to print the variable name and value my_variable.

Custom derive macros

Custom derive Macros in Rust allow you to automatically implement certain traits for structures or enums.

Let's create a simple one derive macro that will implement the trait Description for a structure or enum by providing them with a method describe()returning a string representation:

// в crate для процедурных макросов, в lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    
    let name = input.ident;
    let gen = quote! {
        impl Description for #name {
            fn describe() -> String {
                format!("This is a {}", stringify!(#name))
            }
        }
    };

    gen.into()
}

Using crates syn for parsing incoming TokenStream into the structure DeriveInput, which provides information about the type to which the macro is applied. We use quote! to generate code that implements the trait Description.

Let's use the macro:

// в основном crate

#[derive(Describe)]
struct MyStruct;

trait Description {
    fn describe() -> String;
}

fn main() {
    println!("{}", MyStruct::describe());
}

After adding the macro #[derive(Describe)] To MyStructyou can method describe()which was automatically implemented for MyStruct thanks to the procedural macro.

Attribute Macros

With attribute macros, you can define custom attributes that can be applied to various code elements such as functions, structures, modules, etc. These macros take two arguments: a set of attribute tokens and a token TokenStream element to which the attribute is applied. The result of the attribute macro is a new TokenStreamwhich replaces the original element.

Let's create an attribute macro log_functionwhich will add logging when entering and exiting the function:

extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_function(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input_fn = parse_macro_input!(item as ItemFn);
    let fn_name = &input_fn.sig.ident;
    let fn_body = &input_fn.block;
    
    let result = quote! {
        fn #fn_name() {
            println!("Entering {}", stringify!(#fn_name));
            #fn_body
            println!("Exiting {}", stringify!(#fn_name));
        }
    };

    result.into()
}

The code takes a function that has an attribute macro applied to it and modifies it so that when it is called, function entry and exit messages are printed to the console.

use my_proc_macros::log_function;

#[log_function]
fn my_function() {
    println!("Function body execution");
}

After adding the attribute #[log_function] to function my_functionwhen you call it, the corresponding login and logout messages will be displayed in the console.


My colleagues from OTUS talk about the most popular languages ​​and practical tools as part of practical online courses. Link you can view the full catalog of courses, as well as register for free webinars.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *