Rust function-like procedural Macros

Rust function-like procedural MacrosThis tutorial explains the distinction between functions and function like macros. For example, why does msg! have an exclamation point after it? This tutorial will explain this syntax.As a strongly typed language, Rust cannot accept an arbitrary number of arguments to a function.For example, the Python print function can accept an arbitrary number of arguments:
print(1)
print(1, 2)
print(1, 2, 3)
The ! denotes that the “function” is a function-like macro.Rust function-like macros are identified by the presence of a ! symbol, for example in println!(...) or msg!(...) in Solana.In Rust, a regular function (not function-like macro) to print something is std::io::stdout().write and it only accepts a single byte string as an argument.*If you want to run the following code, the Rust Playground is a convenient tool if you don’t want to set up a development environment.*Let’s use the following example (taken from here):
use std::io::Write;

fn main() {
    std::io::stdout().write(b"Hello, world!\n").unwrap();
}
Note that write is a function, not a macro as it does not have the !.If you try to do what we did above in Python, the code won’t compile because write only accepts one argument:
// this does not compile
use std::io::Write;

fn main() {
    std::io::stdout().write(b"1\n").unwrap();
    std::io::stdout().write(b"1", b"2\n").unwrap();
    std::io::stdout().write(b"1", b"2", b"3\n").unwrap();
}
As such, if you wish to print an arbitrary number of arguments, *you need to write a custom print function to handle each case for each number of arguments — that is extremely inefficient!*Here is what such code would look like (this is highly not recommended!):
use std::io::Write;

// print one argument
fn print1(arg1: &[u8]) -> () {
    std::io::stdout().write(arg1).unwrap();
}

// print two arguments
fn print2(arg1: &[u8], arg2: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

// print three arguments
fn print3(arg1: &[u8], arg2: &[u8], arg3: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2, b" ", arg3].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

fn main() {
    print1(b"1\n");
    print2(b"1", b"2\n");
    print3(b"1", b"2", b"3\n");
}
If we look for a pattern in the print1, print2, print3 functions, it is simply inserting the arguments into a vector and adding a space in between them, then converting the vector back into a bytes string (a bytes slice to be precise).Wouldn’t it be nice if we could take a piece of code like println! and automatically expand it into a print function that takes exactly as many arguments as we need?This is what a Rust macro does.A Rust macro takes Rust code as input and programmatically expands it into more Rust code.This helps us avoid the boredom of having to write a print function for every kind of print statement our code requires.

Expanding the macro

To see an example of how the Rust compiler is expanding the println! macro, check out the cargo expand github repo. The result is quite verbose so we will not show it here.

It’s okay to treat macros as black boxes

Macros are very handy when supplied by a library, but very tedious to write by hand as it requires literally parsing the Rust code.

Different kinds of macros in Rust

The example we have given with println! is a function-like macro. Rust has other kinds of macros but the other two we care about are the *custom derive macro* and the *attribute-like macro*.Let’s look at a fresh program created by anchor:Different kinds of macrosWe will explain how these work in the following tutorial.

Learn more with RareSkills

This tutorial is part of our free Solana course.*Originally Published February, 15, 2024*

Cross Program Invocation In Anchor

Cross Program Invocation In Anchor Cross Program Invocation (CPI) is Solana’s terminology for a program calling the public function of another program. We’ve already done CPI before when we sent a transfer SOL transaction to the system program. Here is the relevant snippet by way of reminder: pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> […]

Reading Another Anchor Program’s Account Data On Chain

Reading Another Anchor Program’s Account Data On Chain In Solidity, reading another contract’s storage requires calling a view function or the storage variable being public. In Solana, an off-chain client can read a storage account directly. This tutorial shows how an on-chain Solana program can read the data in an account it does not own. […]

#[derive(Accounts)] in Anchor: different kinds of accounts

[derive(Accounts)] in Anchor: different kinds of accounts #[derive(Accounts)] in Solana Anchor is an attribute-like macro for structs that holds references to all the accounts the function will access during its execution. In Solana, every account the transaction will access must be specified in advance One reason Solana is so fast is that it executes transactions […]

Modifying accounts using different signers

Modifying accounts using different signers In our Solana tutorials thus far, we’ve only had one account initialize and write to the account. In practice, this is very restrictive. For example, if user Alice is transferring points to Bob, Alice must be able to write to an account initialized by user Bob. In this tutorial we […]