Tuesday, October 11, 2022

Introduction to Declarative Macros in Rust

This post will give a quick overview of declarative macros in Rust. If you are not familiar with the concepts of macros in Rust, then read Introduction to Macros in Rust first.

Declarative macros work similar to how pattern matching works. It involves taking inputs, then pattern matching to determine what logic to execute.

The macros definition for creating declarative macros, then contains rules which determine the Rust code that gets generated/executed based on the pattern.

This is why declarative macros are also sometimes referred to as Macros By Example, as the code that is generated depends on the example of patterns that matches.

Rust provides the macro_rules! macro for defining declarative macros. You use the macro_rules! to specify the input string pattern and also to specify the code that gets generated/executed.

The syntax for macro_rules takes the following form:

macro_rules! $name {
    ($matcher) => {$expansion} ;
    ($matcher) => {$expansion} ;
    // …
   ($matcher) => {$expansion} ;
}

That is, the macro_rules definition contains a list of rules, the particular rule, represented by {$expression} that will be executed depends on which of the ($matcher) matches the input. To see this syntax in action, create a new cargo project by running

cargo new declarative_macros

Open the main.rs and edit it.

The $matcher can be a simple literal match. For example to create a macro that prints different message based on whether the input passed to it is "morning" or "evening" , add the code below to main.rs:

fn main() {

    #[macro_export]
    macro_rules! greet {
        ("morning") => {
            println!("Good morning! Have a nice day!");
        };
        ("evening") => {
            println!("Good evening! Sleep tight!");
        }
    }
}
// using it
#[test]
fn run_greet() {
    // prints "Good morning! Have a nice day!"
    greet!("morning"); 
    // prints "Good evening! Sleep tight!"
    greet!("evening");
}

The $match can be more versatile than just matching a literal. For example the match can be used to define that the input should be an expression, a block of code etc. It can also be used to capture values passed in, that are then used within the macro logic.

To demonstrate how using a more versatile match pattern looks like when defining declarative macros, we write one that retrieves the balances for specified Solana accounts.

For this to work, update the dependencies section in Cargo.toml as follows:

[dependencies]
solana-client = "1.8.1, < 1.11"
solana-program = "1.8.1, < 1.11"

Then update the main.rs file as follows:

use solana_client::rpc_client::RpcClient;
use solana_program::pubkey::Pubkey;
use std::str::FromStr;

fn main() {
 #[macro_export]
 macro_rules! sol_balance {
  ($item:expr) => {
    {
     let mut result: Vec<u64> = Vec::new();
     let rpc = RpcClient::new("https://api.devnet.solana.com"
               .to_string());
     let item_str = format!("{}", $item);
     for i in item_str.split(",") {
         result.push(rpc.get_account(
         &Pubkey::from_str(i.trim()).unwrap()
         ).unwrap().lamports);
       }
       result
     }
  }}

}

#[test]
fn run_macros() {
    let result = sol_balance!(
        "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU,
        HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny"
    );
    dbg!(result);
}

Running the test would print out the sol balance at the specified account. Let's go over the code above. The core structure of the macro is:

    #[macro_export]
    macro_rules! sol_balance {
        ($item:expr) => {
        // body of macro
    }}

The ($item:expr) is an example of a more versatile match. It still corresponds to the ($matcher) in ($matcher) => {$expansion}. The $item is the input that is being matched, but this time around it is not just a literal. This syntax also captures the input making its value available for use in the macro code. This is called Metavariables. It also contains a qualification part using :expr. This is used to categorize the input. The :expr is an example of a fragment specifier. :expr specifies that the input is an expression. Other fragment specifiers exist. See Fragment Specifiers for more information.

The body of the macro is then defined as:

{
  let mut result: Vec&lt;u64&gt; = Vec::new();
  let rpc = RpcClient::new("https://api.devnet.solana.com"
            .to_string());
  let item_str = format!("{}", $item);
  for i in item_str.split(",") {
      result.push(rpc.get_account(
      &amp;Pubkey::from_str(i.trim()).unwrap()
      ).unwrap().lamports);
     }
  result
}

Note that we wrap this within {} to allow using multiple statements.

It is also possible to support repetition when defining the matcher. This take the form of:

$ ( $(matcher) ) separator repmode

You can read this as $(matcher) as the pattern to be repeated, the separator that delineates the repetition, and repmode determines the number of repetition that is expected.

The currently supported repmode are:

?: at most one repetition *: zero or more repetitions +: one or more repetitions

The same syntax is used to process each item that is repeated. To see this repetition in practice, we can update the sol_balance macro to take a list of accounts separated by ";" instead of taking a single string separated by ",". This will look like:

use solana_client::rpc_client::RpcClient;
use solana_program::pubkey::Pubkey;
use std::str::FromStr;

fn main() {

 #[macro_export]
 macro_rules! sol_balance {
   ($($item:expr);*) => {
     {
       let mut result: Vec<u64> = Vec::new();

       let rpc = RpcClient::new("https://api.devnet.solana.com"
                 .to_string());
         $(
           let item_str = format!("{}", $item);
           result.push(rpc.get_account(
           &Pubkey::from_str(item_str.trim()).unwrap()
           ).unwrap().lamports);
          )*
          result
       }
    }}
}

// usage

#[test]
fn run_macros() {
  let result = sol_balance!(
    "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
     ;
     "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny"
   );
  dbg!(result);
}

Running the test with the code modified as above should also print the sol balance at the given accounts.

There is more to writing a declarative macros that is not touched upon in this overview. For example we did not talk about the #[macro_export] attribute, neither did we talk about things like Metavariable Expressions. Even at this, this introduction should be enough to understand the essence of declarative Macros in Rust.

The next post in this series will then be an introduction to the function-like procedural macros.


No comments: