The original title: Macros in Rust: A tutorial with examples the original links: blog.logrocket.com/macros-in-r… Praying for Rust


In this article, we’ll cover everything you need to know about Rust macros, including an introduction to them and examples of how to use them.

We will cover the following:

  • What is Rust macro?

  • The type of the Rust macro

  • A declaration of the macro Rust

    • Create declarative macros
    • Advanced parsing of declarative macros in Rust
    • Parse metadata from a structure
    • Limitations of declarative macros
  • Process macros in Rust

    • Attribute style macros
    • Custom inheritance macros
    • Functional style macros

What is Rust macro?

Rust has great support for Macro. Macros enable you to generate code the way you write code, which is often called metaprogramming.

Macros provide function-like functionality, but without the runtime overhead. However, because the macro expands at compile time, it incurs some compile time overhead.

Rust macros are very different from those in C. While Rust macros are applied to the token tree, C macros are text replacements.

The type of the Rust macro

Rust has two types of macros:

  • Declarative macros allow you to write something like a match expression to manipulate the Rust code you provide. It uses the code you provide to generate the code to replace the macro call.

  • Procedural macros allow you to manipulate an abstract syntax tree (AST) of a given Rust code. Procedure macros are functions that pass from one (or two) tokenstreams to another, replacing the macro call with the output.

Let’s look at declarative and procedural macros in more detail, and discuss some examples of how macros can be used in Rust.

Declarative macros in Rust

Macros are created by using macro_rules! To declare. Declarative macros, while relatively weak in function, provide an easy-to-use interface to create macros to remove repetitive code. One of the most common declarative macros is println! . Declarative macros provide a match-like interface where the macro is replaced with the code that matches the branch.

Create declarative macros

macro_rules! add{
 // macth like arm for macro
    ($a:expr,$b:expr)=>{
 // macro expand to this code
        {
// $a and $b will be templated using the value/variable provided to macro
            $a+$b
        }
    }
}

fn main() {// call to macro, $a=1 and $b=2add! (1.2);
}
Copy the code

This code creates a macro to add two numbers. [macro_rules!] Use with the name of the macro, add, and the macro body.

This macro does not add two numbers; it simply replaces itself with code that adds two numbers. Each branch of the macro takes an argument to a function, and the argument can be of multiple types. If we wanted the add function to accept only one argument, we could add another branch:

macro_rules! add{
 // first arm match add! (1, 2), the add! (2, 3) etc
    ($a:expr,$b:expr)=>{
        {
            $a+$b
        }
    };
// Second arm macth add! (1), add! (2) etc
    ($a:expr)=>{
        {
            $a
        }
    }
}

fn main() {// call the macro
    let x=0; add! (1.2); add! (x); }Copy the code

There can be multiple branches within a macro, which expands to different code based on different parameters. Each branch can accept multiple arguments starting with a $sign followed by a token type:

  • Item – An item, such as a function, structure, module, etc.

  • Block — A block (that is, a statement block or an expression surrounded by curly braces)

  • STMT — a statement

  • Pat — A Pattern

  • Expr — An expression

  • Ty — A type

  • Ident – an identifier (Indentfier)

  • Path — a path (for example, foo, :: STD ::mem: replace, Transmute ::<_, int>,…)

  • Meta — a metadata item; Located at # […]. And #! […]. attribute

  • Tt – a lexical tree

  • Vis — a undefined qualifier that may be empty

In the example above, we use the $TYp parameter, which has a token type of TY, similar to U8, U16. This macro converts to a specific type before adding the numbers.

macro_rules! add_as{
// using a ty token type for macthing datatypes passed to maccro
    ($a:expr,$b:expr,$typ:ty)=>{
        $a as $typ + $b as $typ
    }
}

fn main() {println!("{}",add_as! (0.2.u8));
}
Copy the code

Rust macros also support receiving a variable number of parameters. This operation is very similar to regular expressions. * is used for zero or more token types, and + is used for zero or one parameter.

macro_rules! add_as{
    (
  // repeated block
  $($a:expr)
 // seperator
   ,
// zero or more*) = > {{// to handle the case without any arguments
   0
   // block to be repeated
   $(+$a)*
     }
    }
}

fn main() {println!("{}",add_as! (1.2.3.4)); // => println! (" {} ", {0 + 1 + 2 + 3 + 4})
}
Copy the code

Duplicate token types are wrapped in $(), followed by a delimiter and an * or a + to indicate how many times the token will be repeated. Delimiters are used to distinguish multiple tokens from one another. $() followed by * and + are used to indicate duplicate code blocks. In the example above, +$a is a repetitive piece of code.

If you look more closely, you’ll see that this code has an extra 0 to make the syntax valid. To remove the 0 and make the add expression look like a parameter, we need to create a new macro called TT Muncher.

macro_rules! add{
 // first arm in case of single argument and last remaining variable/number
    ($a:expr)=>{
        $a
    };
// second arm in case of two arument are passed and stop recursion in case of odd number ofarguments
    ($a:expr,$b:expr)=>{
        {
            $a+$b
        }
    };
// add the number and the result of remaining arguments($a:expr,$($b:tt)*)=>{ { $a+add! ($($b)*) } } }fn main() {println!("{}",add! (1.2.3.4));
}
Copy the code

TT Muncher processes each token individually recursively, making it easier to process a single token at a time. This macro has three branches:

  • The first branch deals with whether a single parameter passes

  • The second branch deals with whether two parameters pass

  • The third branch calls the Add macro again with the remaining arguments

Macro arguments do not need to be comma-separated. Multiple tokens can be used for different token types. For example, parentheses can be used in conjunction with identToken types. The Rust compiler can match the corresponding branches and export variables from the argument string.

macro_rules! ok_or_return{
/ / match something (q, r, t, June), etc
// compiler extracts function name and arguments. It injects the values in respective varibles.
    ($a:ident($($b:tt)*))=>{
       {
        match $a($($b)*) {
            Ok(value)=>value,
            Err(err)=>{
                return Err(err); }}}}; }fn some_work(i:i64,j:i64) - >Result< (i64.i64),String> {if i+j>2 {
        Ok((i,j))
    } else {
        Err("error".to_owned())
    }
}

fn main() - >Result< (),String>{ ok_or_return! (some_work(1.4)); ok_or_return! (some_work(1.0));
    Ok(())}Copy the code

Ok_or_return This macro implements the ability to return Err if the function operation it receives returns Err, or if the operation returns Ok, it returns the value in Ok. It takes a function as an argument and executes it in a match statement. It is repeated for functions passed to parameters.

In general, few macros are combined into one macro. In these rare cases, internal macro rules are used. It helps to manipulate these macro inputs and write neat TT Munchers.

To create an internal rule, add the rule name starting with @ as an argument. This macro will not match an internal rule unless explicitly specified as a parameter.

macro_rules! ok_or_return{
 // internal rule.
    (@error $a:ident,$($b:tt)* )=>{
        {
        match $a($($b)*) {
            Ok(value)=>value,
            Err(err)=>{
                return Err(err); }}}};// public rule can be called by the user.($a:ident($($b:tt)*))=>{ ok_or_return! (@error $a,$($b)*) }; }fn some_work(i:i64,j:i64) - >Result< (i64.i64),String> {if i+j>2 {
        Ok((i,j))
    } else {
        Err("error".to_owned())
    }
}

fn main() - >Result< (),String> {// instead of round bracket curly brackets can also be usedok_or_return! {some_work(1.4)}; ok_or_return! (some_work(1.0));
    Ok(())}Copy the code

Use declarative macros for advanced parsing in Rust

Macros sometimes perform tasks that require parsing the Rust language itself.

Let’s create a macro that combines all the concepts we’ve covered so far and makes it public with the pub keyword.

First, we need to parse the Rust structure to get the name of the structure, its fields, and its field types.

Parse the name of the structure and its fields

A struct (that is, structure) declaration has a visibility keyword (such as pub) at the beginning, followed by the struct keyword, then the name of the struct and the body of the struct.

macro_rules! make_public{
    (
  // use vis type for visibility keyword and ident for struct name
     $vis:vis struct $struct_name:ident { }
    ) => {
        {
            pub struct $struct_name{}}}}Copy the code

$vis will have visibility, and $struct_name will have a structure name. To make a structure public, we simply add the pub keyword and ignore the $VIS variable.

A struct may contain multiple fields that have the same or different data types and visibility. The TY token type is used for data types, VIS for visibility, and IDENT for field names. We will use * for zero or more fields.

 macro_rules! make_public{
    (
     $vis:vis struct $struct_name:ident {
        $(
 // vis for field visibility, ident for field name and ty for field data type
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*
    }
    ) => {
        {
            pub struct $struct_name{$(pub $field_name : $field_type,
                )*
            }
        }
    }
}
Copy the code

fromstructParse metadata in

Often, structs have some additional metadata or process macros, such as #[derive(Debug)]. This metadata needs to be kept intact. Parsing this type of metadata is done using meta types.

macro_rules! make_public{
    (
     // meta data about struct
     $(#[$meta:meta])*
     $vis:vis struct $struct_name:ident {
        $(
        // meta data about field
        $(#[$field_meta:meta])*
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*$(,)+
    }
    ) => {
        {
            $(#[$meta]) *pub struct $struct_name{$($(#[$field_meta:meta]) *pub $field_name : $field_type,
                )*
            }
        }
    }
}
Copy the code

Our make_public macro is now ready. To see how make_public works, let’s use Rust Playground to expand the macro into actual compiled code.

macro_rules! make_public{
    (
     $(#[$meta:meta])*
     $vis:vis struct $struct_name:ident {
        $(
        $(#[$field_meta:meta])*
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*$(,)+
    }
    ) => {

            $(#[$meta]) *pub struct $struct_name{$($(#[$field_meta:meta]) *pub $field_name : $field_type,
                )*
            }
    }
}

fn main(){ make_public! {#[derive(Debug)]
        struct Name{
            n:i64,
            t:i64,
            g:i64,}}}Copy the code

The expanded code looks like this:

// some imports


macro_rules! make_public {
    ($ (#[$ meta : meta]) * $ vis : vis struct $ struct_name : ident
     {
         $
         ($ (#[$ field_meta : meta]) * $ field_vis : vis $ field_name : ident
          : $ field_type : ty), * $ (,) +
     }) =>
    {

            $ (#[$ meta]) * pub struct $ struct_name{$($(#[$ field_meta : meta]) * pub $ field_name : $
                 field_type,) *
            }
    }
}

fn main() {
        pub struct name {
            pub n: i64.pub t: i64.pub g: i64,}}Copy the code

Limitations of declarative macros

Declarative macros have some limitations. Some are related to Rust macros themselves, while others are specific to declarative macros:

  • Lack of support for auto-completion and expansion of macros

  • Declarative macromodulation is difficult

  • Limited ability to modify

  • Bigger binary

  • Longer compile times (this applies to both declarative and procedural macros)