Rust Concepts for Writing Solana Programs

Rust Concepts for Writing Solana Programs

Developers who turn to Solana do so because Solana has the ability to process many transactions per second. This is possible because Solana does not use only Proof-of-Work (PoW) or Proof-of-Stake (PoS) consensus algorithms like Bitcoin or Ethereum do. Instead, it uses an alternative consensus algorithm called Proof-of-History (PoH) which doesn't require massive amounts of energy like PoW or PoS does.

Solana programs can be written in C, C++ and Rust programming languages. These programs are similar to smart contracts in Ethereum. In this article, you will learn some key concepts in Rust you need to know to write solana programs because unlike C and C++, Rust is a low-level language that gives the feel of a high-level language. Rust automatically takes care of low-level operations, but also exposes these low-level operations to developers. This makes Rust language fast, powerful, and easy to write.

Rust Concepts for Solana Programs

  • Variable ownership
  • Struct
  • Vector, String, and HashMaps
  • Enums and pattern matching
  • Error handling
  • Packages, crates, and modules in Rust

Variable Ownership

Rust variables, like any other programming language, have a life that is determined by the scope in which they are defined. This scope can be either local or global. Rust variables can also be mutable, immutable, or shadowed.

  • Mutable: These variables are assigned using the "let" keyword, and they can be reassigned. They can also be changed if their values are mutable. An example of a mutable variable is one whose value may change during the lifetime of the program. Examine the code below:
let mut greetings = "hello";

greetings = "Hello world";
  • Immutable: These variables are assigned using the "const" keyword, and they cannot be reassigned at any point in the program. To make an immutable variable, add the "mut" keyword before it. An example of an immutable variable is one whose value will not change during the lifetime of the program. Sample code:
// variables are immutable by default
let greetings = "hello";

// this will throw an error
greetings = "Hello world";

// You can make a variable mutable by adding the
// "mut" keyword

let mut action = "dancing";

// this would work
action = "singing"; 

// variables defined with the "const" keyword cannot 
// be reassigned or made mutable

const MY_ACTION = "Walking";

// this will throw an error
MY_ACTION = "Dancing";
  • Shadowing: This is simply a way of showing that you want to reassign a variable to a new one in Rust without changing its value or type. It involves declaring another variable with the same name as the old one and assigning it some value or expression that evaluates that value. Sample code:
fn main() {
    let count = 5;

    let count = count + 1;

    println!("The value of count is: {}", count );
}

There are two types of variable ownership in Rust. These types are called, creatively enough, "Owning" and "Borrowing."

An owning variable owns the data it points to, so the data will be destroyed when that variable goes out of scope. Borrowed variables do not own the data they point to, so this data will not be destroyed when that variable goes out of scope.

You can have both owning and borrowing variables referring to the same piece of data, but you cannot have two owning variables or two borrowing variables referring to the same piece of data at the same time. This is because if you had multiple owning variables pointing to a single piece of data and all those variables went out of scope at the same time, Rust wouldn't know which one(s) should be responsible for destroying that piece of data.

It would be like multiple people trying to eat a single hamburger: how do you decide who gets to keep it? Similarly, if two borrowing variables were pointing to a single piece of data and both tried to change it (for example by assigning a new value), Rust wouldn't know which one(s) should "win" over the other(s). This could easily lead to subtle bugs in your program where one borrows. Here is an example below:

fn main() {
  // You can declare your variable here
    let action = String::from("Walking");

// You need to pass the variable into the "calculate_length" function,
// if pass it like this
// let len = calculate_length(action);
// This means you are transferring the ownership of "action" 
// variable to the function "calculate_length",
// this means the variable would no longer be
// available in the "main" function scope again.

// But since you still need to use the variable 
// in the "println!" macro below, instead of transferring ownership,
// you will send a borrowed reference of the 
// variable to the "calculate_length" function.

    let len = calculate_length(&action); 

    println!("The length of '{}' is {}.", action, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Struct

In Rust, a struct is a custom data type that lets you define the data that you'll use in your code. It's a collection of variables (called fields) that can store different kinds of related information.

Let's say you're building an app that lets users create a profile and track their favorite locations to have brunch. You'll want to keep track of each user's name, age, email address, and the number of times they've been to brunch this month. You can combine these variables into one data type called Profile, like this:

struct Profile {

    name: String,

    age: i32,

    email: String,

    times_been_to_brunch: i32

}

You will use Struct to define and structure on-chain data when building Solana programs.

Vector, String, and HashMaps

These are custom variables in Rust and are known as Collections.

Vectors are like arrays. But unlike arrays, they can grow and shrink based on what's added to them or removed from them and you don't have to specify the length when you declare them.

Vectors in Rust are helpful for when you want to store data in a contiguous area of memory. When you create a vector, it allocates memory along a single, continuous region of space that's guaranteed to be big enough to hold the amount of data you're asking it to. The only downside of vectors is that they can only store one type of data and they don't have key-value pairs. In Solana programs, Vectors are used to collect and handle instructions passed into the program. Here is an example of a vector:

 let my_vector = vec![1, 2, 3];

my_vector.push(4);
my_vector.push(5);

Strings are collections of characters—like "Hello world!". They are another way to store data, but unlike vectors, strings are growable. That means you don't have to specify how big they'll be upfront (which is nice if you don't know how much data you'll need). Like vectors, strings can only store one type of data. But unlike vectors, strings are complicated—they're composed of multiple parts which help with things like indexing and slicing. Here is an example:

 let hello = String::from("السلام عليكم");
 let hello = String::from("Dobrý den");
 let hello = String::from("Hello");
 let hello = String::from("שָׁלוֹם");
 let hello = String::from("नमस्ते");
 let hello = String::from("こんにちは");
 let hello = String::from("안녕하세요");
 let hello = String::from("你好");
 let hello = String::from("Olá");
 let hello = String::from("Здравствуйте");
 let hello = String::from("Hola");

HashMaps allows you to use keys and values for storing information. They're kind of like dictionaries: the key is the word and the value is the definition. Unlike strings and vectors, HashMaps can store different types of keys and values. But because keys are stored in buckets (which means the same bucket could contain multiple keys), HashMaps aren't great for keeping track of ordering. Here is an example:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

Enums and pattern matching

Enums are a way of grouping together different kinds of data. For example, you could have an enum that represents all the possible playing cards in a deck, like:

enum Card {
   Club(u8),
   Spade(u8), 
   Heart(u8), 
   Diamond(u8)
}

pattern matching:If you want to know what the highest-value card is in a player's hand in your card game, you can use pattern matching to write a function that takes a vector of Cards and returns the highest-value one. For example:

fn main() {

let card_at_hand = [
   Card::Club(4),
   Card::Spade(6),
   Card::Heart(3),
   Card::Diamond(11)
]; 

  let highest_card =    find_highest_card(&card_at_hand);

  println!("The largest card is: {:?}", highest_card);
}

#[derive(Debug)]
enum Card {
   Club(u8),
   Spade(u8), 
   Heart(u8), 
   Diamond(u8)
}



  // This function will get the highest card from a vector collection of different cards
  // passed into it as an argument.
fn find_highest_card(hand: &[Card]) -> Card {

// create a mutable variable that will hold the largest card
// by default you can create a dummy card
     let mut high_card = Card::Club(0);

     let mut highest_number = 0;

     // now lets loop through the card and use 'match' to filter out the 
     // highest card
     for card in hand.iter() {
        match card {
             // if this card matches a club, get its number and check if it is the highest 
             Card::Club(num) if *num > highest_number => {
                 high_card = Card::Club(*num);
                 highest_number = *num;
             },
             // if this card matches a spade, get its number and check if it is the highest
             Card::Spade(num) if *num > highest_number => {
                 high_card = Card::Spade(*num);
                 highest_number = *num;
             },
             // if this card matches a heart, get its number and check if it is the highest
             Card::Heart(num) if *num > highest_number => {
                 high_card = Card::Heart(*num);
                 highest_number = *num;
             },
             // if this card matches a diamond, get its number and check if it is the highest
             Card::Diamond(num) if *num > highest_number => {
                 high_card = Card::Diamond(*num);
                 highest_number = *num;
             },
             // if the card doesn't fit any of the above conditions, do nothing
             _ => ()
        }

     }

     return high_card;
}

In the Solana program, Enum is useful for matching instructions to the right function that would execute the instruction. It works like a web router in many web programming frameworks.

Error handling

Rust has a fairly simple set of tools for error handling.

Rust has a very simple mechanism for handling errors: the Result type, which is an enum that can have either an 'Ok' or an 'Err' variant. Result types can be used in conjunction with functions that have returned, and when those functions are called, the return value of the function will be a Result type. Below is an example of how this works with a couple of functions with the first function returning an integer:

fn add(a: i32, b: i32) -> i32 {

a + b

}

This function will return an integer when you call it, but if you wanted to use it in a situation where the user could input something other than an integer, you can adjust the function to return a Result instead:

fn add(a: i32, b: i32) -> Result<i32> {

Ok(a + b)

}

In Solana programs, you will use the 'Ok' and 'Err' variants of the Result Enum to handle return results or handle errors.

Packages, crates, and modules in Rust

Packages help developers organize their libraries and modules help us organize our code.

Packages are collections of crates. These crates are "libraries" or "modules" that can be linked to other crates. Crates have a special root module named after the crate's base directory which contains all the public items in the crate. A single crate can contain multiple files, but there can only be one crate per directory.

Rust has two kinds of crates: binary and library. The main function inside of a binary crate will be compiled into an executable file that runs as a stand-alone file. Binary crates cannot be linked to other crates as library crates can. Instead, they will be run from the command line with the command ".exe". Library crates cannot contain any main functions and will instead be compiled into a ".dll" file (on Windows) or ".so" file (on Unix). The .dll or .so file contains code that is linked to other library crates for use by others.

You will make use of many Solana libraries when building a program, which will provide some helper functions that would make building easier.

Conclusion

In the article, you learned about concepts in Rust used in writing Solana Programs. Concepts such as :

  • Variable ownership
  • Struct
  • Vector, String, and HashMaps
  • Enums and pattern matching
  • Error handling
  • Packages, crates, and modules in Rust. For further information, you can read Rust documentation. In the next article, you will learn how to build a Solana program and launch it on the Solana test blockchain.

Lets connect on Twitter @drayfocus and on LinkedIn Akinola Ayomide.

Cheers!