Rust Structs and Attribute-like and Custom Derive Macros

Rust attribute and custom-derive macros

Attribute-like and custom derive macros in Rust are used to take a block of Rust code and modify it in some way at compile time, often to add functionality.

To understand attribute-like and custom derive macros in Rust, we first need to briefly cover implementation structs in Rust.

Implementations for structs: impl

The following struct should be straightforward to understand. What gets interesting is when we create functions that operate on a particular struct. The way we do this is with impl:

struct Person {
    name: String,
    age: u8,
}

Associated functions and methods are implemented for structs inside the impl block.

Associated functions can be compared to the scenario in Solidity where a library is created for interacting with a struct. When we define using lib for MyStruct, it allows us to use the syntax myStruct.associatedFunction(). This gives the function access to myStruct via the Self keyword.

We recommend using the Rust Playground but for more complex examples, you may have to set up your IDE.

Let’s look at an example below:

struct Person {
    age: u8,
    name: String,
}

// Implement a method `new()` for the `Person` struct, allowing initialization of a `Person` instance
impl Person {
    // Create a new `Person` with the provided `name` and `age`
    fn new(name: String, age: u8) -> Self {
        Person { name, age }
    }
    
    fn can_drink(&self) -> bool {
        if self.age >= 21 as u8 {
            return true;
        }
        return false;
    }

    fn age_in_one_year(&self) -> u8 {
        return &self.age + 1;
    }
} 

fn main() {
    // Usage: Create a new `Person` instance with a name and age
    let person = Person::new(String::from("Jesserc"), 19);

    // use some impl functions
    println!("{:?}", person.can_drink()); // false
    println!("{:?}", person.age_in_one_year()); // 20
    println!("{:?}", person.name);
}

Usage:

// Usage: Create a new `Person` instance with a name and age
let person = Person::new(String::from("Jesserc"), 19);

// use some impl functions
person.can_drink(); // false
person.age_in_one_year(); // 20

Rust Traits

Rust traits are a way to implement shared behavior among different impls. Think of them like an interface or abstract contract in Solidity — any contract that uses the interface must implement certain functions.

For instance, let’s say we have a scenario where we need to define a Car and Boat struct. We want to attach a method that allows us to retrieve their speed in kilometers per hour. In Rust, we can accomplish this by using a single trait and sharing the method between the two structs.

This is shown below:

// Traits are defined with the `trait` keyword followed by their name
trait Speed {
    fn get_speed_kph(&self) -> f64;
}

// Car struct
struct Car {
    speed_mph: f64,
}

// Boat struct
struct Boat {
    speed_knots: f64,
}

// Traits are implemented for a type using the `impl` keyword as shown below
impl Speed for Car {
    fn get_speed_kph(&self) -> f64 {
        // Convert miles per hour to kilometers per hour
        self.speed_mph * 1.60934
    }
}

// We also implement the `Speed` trait for `Boat`
impl Speed for Boat {
    fn get_speed_kph(&self) -> f64 {
        // Convert knots to kilometers per hour
        self.speed_knots * 1.852
    }
}

fn main() {
    // Initialize a `Car` and `Boat` type
    let car = Car { speed_mph: 60.0 };
    let boat = Boat { speed_knots: 30.0 };

    // Get and print the speeds in kilometers per hour
    let car_speed_kph = car.get_speed_kph();
    let boat_speed_kph = boat.get_speed_kph();

    println!("Car Speed: {} km/h", car_speed_kph); // 96.5604 km/h
    println!("Boat Speed: {} km/h", boat_speed_kph); // 55.56 km/h
}

How macros can modify structs

In our tutorial on function-like macros, we saw how macros can expand code like println!(...) and msg!(...) in large Rust code. The other kind of macros we care about in the context of Solana is the attribute-like macro and the derive macro. We can see all three (function-like, attribute-like, and derive) macros in the starter program anchor creates:

Rust attribute and custom-derive macros

To get an intuition for what the attribute-like macros is doing, we will create two macros: one to add fields to a struct and another that removes them.

Example 1: attribute-like macro, inserting fields

To gain a better understanding of how Rust attributes and macros work, we will create an attribute-like macro that:

  1. takes a struct which does not have the fields foo and bar, of type i32
  2. inserts those fields into the struct
  3. creates an impl with a function called double_foo which returns returns twice the integer value of whatever foo is holding.

Setup

First we create a new Rust project:

cargo new macro-demo --lib 
cd macro-demo
touch src/main.rs

Add the following to the Cargo.toml file:

[lib]
proc-macro = true

[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

Creating the main program

Paste the following code into src/main.rs. Be sure to read the comments:

// src/main.rs
// Import the macro_demo crate and bring all items into scope with the `*` wildcard
// (basically everything in this crate, including our macro in `src/lib.rs`
use macro_demo::*;

// Apply the `foo_bar_attribute` procedural attribute-like macro we created in `src/lib.rs` to `struct MyStruct`
// The procedural macro will generate a new struct definition with specified fields and methods
#[foo_bar_attribute]
struct MyStruct {
    baz: i32,
}

fn main() {
    // Create a new instance of `MyStruct` using the `default()` method
    // This method is provided by the `Default` trait implementation generated by the macro
    let demo = MyStruct::default();

    // Print the contents of `demo` to the console
    // The `Debug` trait implementation generated by the macro allows formatted output with `println!`
    println!("struct is {:?}", demo);

    // Call the `double_foo()` method on `demo`
    // This method is generated by the macro and returns double the value of the `foo` field
    let double_foo = demo.double_foo();

    // Print the result of calling `double_foo` to the console
    println!("double foo: {}", double_foo);
}

Some observations:

  • The struct MyStruct does not have the fields foo in it.
  • The function double_foo is not defined anywhere in the code above, it is assumed to exist.

Now let’s create the attribute-like macro which will modify the MyStruct behind the scenes.

Replace the code in src/lib.rs with the following code (be sure to read the comments):

// src/lib.rs
// Importing necessary external crates
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

// Declaring a procedural attribute-like macro using the `proc_macro_attribute` directive
// This makes the macro usable as an attribute

#[proc_macro_attribute]
// The function `foo_bar_attribute` takes two arguments:
// _metadata: The arguments provided to the macro (if any)
// _input: The TokenStream the macro is applied to
pub fn foo_bar_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    // Parse the input TokenStream into an AST node representing a struct
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // Get the name of the struct

    // Constructing the output TokenStream using the quote! macro
    // The quote! macro allows for writing Rust code as if it were a string,
    // but with the ability to interpolate values
    TokenStream::from(quote! {
        // Derive Debug trait for #struct_name to enable formatted output with `println()`
        #[derive(Debug)]
        // Defining a new struct #struct_name with two fields: foo and bar
        struct #struct_name {
            foo: i32,
            bar: i32,
        }

        // Implementing the Default trait for #struct_name
        // This provides a default() method to create a new instance of #struct_name
        impl Default for #struct_name {
            // The default method returns a new instance of #struct_name
            // with foo set to 10 and bar set to 20
            fn default() -> Self {
                #struct_name { foo: 10, bar: 20}
            }
        }

        impl #struct_name {
            // Defining a method double_foo for #struct_name
            // This method returns double the value of foo
            fn double_foo(&self) -> i32 {
                self.foo * 2
            }
        }
    })
}

Now, to test our macro we run the code in main.rs with cargo run src/main.rs.

We get this output:

struct is MyStruct { foo: 10, bar: 20 }
double foo: 20

Example 2: an attribute-like macro, removing fields

The best way to think about attribute-like macros is that they have unlimited power in how they modify the struct. Let’s repeat the example above, but this time the attribute-like macro will remove all the fields from the struct.

Replace src/lib.rs with the following:

// src/lib.rs
// Importing necessary external crates
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

#[proc_macro_attribute]
pub fn destroy_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // Get the name of the struct

    TokenStream::from(quote! {
        // This returns an empty struct with the same name
        #[derive(Debug)]
        struct #struct_name {
        }
    })
}

Replace src/main.rs with the following:

use macro_demo::*;

#[destroy_attribute]
struct MyStruct {
    baz: i32,
    qux: i32,
}

fn main() {
    let demo = MyStruct { baz: 3, qux: 4 };

    println!("struct is {:?}", demo);
}

When you try to compile it with cargo run src/main.rs you will get the following error:

Error: struct `MyStruct` has no field named `baz`

It may seem odd, because the struct clearly has those fields. However, the attribute-like macro removed them!

The #[derive(…)] macro

The #[derive(…)] macro is much less powerful than the attribute-like macro. For our purposes, a derive macro augments a struct, it does not alter it. (This is not a precise definition, but it is sufficient for now).

A derive macro can, among other things, attach an impl to a struct.

For example, if we try to do the following:

struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

The code will not compile because structs are not “printable.”

To make them printable, they need an impl with a function fmt which returns a string representation of the struct.

If we do the following instead:

#[derive(Debug)]
struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

We expect it to print:

Foo { bar: 3 }

The derive attribute “augmented” Foo in such a way that println! could create a string representation for it.

Summary

An impl is a group of functions that operate on a struct. They are “attached” to the struct by using the same name as the struct. A trait enforces that an impl implements certain functions. In our example, we attached the the trait Speed to impl Car using the syntax impl Speed for Car.

An attribute-like macro takes in a struct and can completely rewrite it.

A derive macro augments a struct with additional functions.

Macros allow Anchor to hide complexity

Let’s look at the program anchor creates during anchor init again:

Rust attribute and custom-derive macros

The attribute #[program] is modifying the module behind the scenes. For example, it implements a router that automatically directs incoming blockchain instructions to the appropriate functions within the module.

The struct Initialize {} is augmented with additional functionality to be used in the Solana framework.

Summary

Macros a very large topic. Our intent here is to give you a sense of what is happening when you see #[program] or #[derive(Accounts)]. Don’t be discouraged if it feels foreign. You do not need to be able to write macros to write Solana programs.

Having an idea of what they do however will hopefully make the programs you see less mysterious.

Learn more with RareSkills

This tutorial is part of our free Solana course.

Originally Published February, 16, 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 […]