rust

Master Rust – From Beginner to Professional
RUST

Master Rust

From Newbie to Professional

Build Your Systems Programming Career


Authored by William H. Simmons
Founder of A Few Bad Newbies LLC

Rust Professional Development Course

Module 1: Rust Fundamentals

Chapter 1: Rust Basics

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety. It combines low-level control with high-level convenience.

// This is a simple Rust program
fn main() {
    println!("Hello, world!");
}

Common Mistakes

  • Forgetting semicolons at the end of statements
  • Mismatched types in variable assignments
  • Not handling Result or Option types properly
  • Attempting to mutate immutable variables

Practice a basic program:

fn main() {
    println!("Welcome to Rust!");
}

Chapter 2: Variables and Mutability

Rust has a strong, static type system with type inference. Variables are immutable by default.

let x = 5; // immutable
let mut y = 10; // mutable
const MAX_POINTS: u32 = 100_000;

Pro Tip

Use immutable variables by default to prevent bugs and enhance code clarity.

Practice variable declaration:

let z = 42;
let mut w = 100;
w = 200;
println!("z: {}, w: {}", z, w);

Chapter 3: Data Types

Rust is statically typed, requiring all variable types to be known at compile time.

let integer: i32 = 42;
let float: f64 = 3.14;
let boolean: bool = true;
let character: char = 'z';
let tuple: (i32, f64, u8) = (500, 6.4, 1);
let array: [i32; 5] = [1, 2, 3, 4, 5];

Pro Tip

Use type inference to reduce verbosity when types are obvious.

Practice data types:

let num: i32 = 10;
let flag: bool = false;
println!("num: {}, flag: {}", num, flag);

Module 2: Control Flow

Chapter 1: If Expressions

Rust’s if expressions can return values, making them more versatile than in many languages.

let number = 6;
if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else {
    println!("number is not divisible by 4 or 3");
}
let condition = true;
let number = if condition { 5 } else { 6 };

Pro Tip

Use if expressions to assign values directly, avoiding verbose ternary-like patterns.

Practice an if expression:

let x = 10;
let result = if x > 5 { "Big" } else { "Small" };
println!("{}", result);

Chapter 2: Loops

Rust supports loop, while, and for loops for different iteration needs.

loop {
    println!("again!");
    break;
}
let mut number = 3;
while number != 0 {
    println!("{}!", number);
    number -= 1;
}
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("the value is: {}", element);
}

Common Mistakes

  • Forgetting to use .iter() for iterating over arrays or vectors
  • Creating infinite loops without a break

Practice a for loop:

let arr = [1, 2, 3];
for x in arr.iter() {
    println!("{}", x);
}

Chapter 3: Pattern Matching

The match operator compares a value against patterns, ensuring exhaustive handling.

enum Coin { Penny, Nickel, Dime, Quarter(State) }
let coin = Coin::Penny;
let value = match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter(state) => {
        println!("State quarter from {:?}!", state);
        25
    },
};
let five = Some(5);
let six = plus_one(five);
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

Pro Tip

Use match for exhaustive pattern matching to avoid runtime errors.

Practice pattern matching:

let x = Some(10);
let y = match x {
    Some(n) => n * 2,
    None => 0,
};
println!("y: {}", y);

Module 3: Ownership and Borrowing

Chapter 1: Ownership

Ownership ensures memory safety without a garbage collector by enforcing strict rules.

let s1 = String::from("hello");
let s2 = s1;
let s1 = String::from("hello");
let s2 = s1.clone();
let s = String::from("hello");
takes_ownership(s);
fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

Common Mistakes

  • Using a variable after it has been moved
  • Not cloning when ownership needs to be retained

Practice ownership:

let s = String::from("test");
let t = s.clone();
println!("{}", t);

Chapter 2: References and Borrowing

References allow accessing data without taking ownership.

let s1 = String::from("hello");
let len = calculate_length(&s1);
fn calculate_length(s: &String) -> usize {
    s.len()
}
let mut s = String::from("hello");
change(&mut s);
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Common Mistakes

  • Creating dangling references
  • Violating borrowing rules
  • Mutating immutable references

Practice borrowing:

let mut s = String::from("hi");
let r = &mut s;
r.push_str(" there");
println!("{}", s);

Chapter 3: Slices

Slices reference a contiguous sequence of elements.

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

Pro Tip

Use slices for safe, zero-copy access to portions of data structures.

Practice slices:

let s = String::from("rust");
let slice = &s[0..2];
println!("{}", slice);

Module 4: Structs and Enums

Chapter 1: Structs

Structs group related data with named fields.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}
let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

Pro Tip

Use tuple structs for simple data groupings without named fields.

Practice a struct:

struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
println!("x: {}, y: {}", p.x, p.y);

Chapter 2: Enums

Enums define a type by listing its variants.

enum IpAddrKind { V4, V6 }
let four = IpAddrKind::V4;
enum IpAddr {
    V4(String),
    V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let some_number = Some(5);
let absent_number: Option<i32> = None;

Pro Tip

Use Option and Result enums to handle nullable values and errors safely.

Practice an enum:

enum Test { A, B }
let t = Test::A;
println!("{:?}", t);

Chapter 3: Methods and Associated Functions

Methods are functions defined within a struct or enum’s context.

struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}
let rect1 = Rectangle { width: 30, height: 50 };
println!("The area is {} square pixels.", rect1.area());
let sq = Rectangle::square(3);

Pro Tip

Use associated functions for constructors or utility methods that don’t require an instance.

Practice a method:

struct Square { side: u32 }
impl Square {
    fn area(&self) -> u32 {
        self.side * self.side
    }
}
let s = Square { side: 4 };
println!("Area: {}", s.area());

Module 5: Collections

Chapter 1: Vectors

Vectors store multiple values in a single, growable data structure.

let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
let mut v = Vec::new();
v.push(5);
let third: &i32 = &v[2];
let third = v.get(2);
for i in &v {
    println!("{}", i);
}
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}
let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Pro Tip

Use vec! macro for concise vector initialization.

Practice a vector:

let mut v = vec![1, 2];
v.push(3);
println!("{:?}", v);

Chapter 2: Strings

Rust’s String is mutable and owned; &str is a string slice.

let mut s = String::new();
let s = "initial contents".to_string();
let mut s = String::from("foo");
s.push_str("bar");
s.push('l');
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
let s = format!("{}-{}-{}", "tic", "tac", "toe");
let hello = "Здравствуйте";
let s = &hello[0..4];
for c in "नमस्ते".chars() {
    println!("{}", c);
}

Common Mistakes

  • Indexing strings directly due to UTF-8 encoding
  • Not understanding ownership when concatenating strings

Practice string manipulation:

let mut s = String::from("hi");
s.push_str(" there");
println!("{}", s);

Chapter 3: Hash Maps

Hash maps map keys to values using a hash function.

use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
for (key, value) in &scores {
    println!("{}: {}", key, value);
}
scores.insert(String::from("Blue"), 25);
scores.entry(String::from("Yellow")).or_insert(50);
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

Pro Tip

Use the entry API to avoid redundant key lookups in hash maps.

Practice a hash map:

use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("key", 1);
println!("{:?}", m);

Module 6: Error Handling

Chapter 1: Unrecoverable Errors

Unrecoverable errors cause the program to panic and stop execution.

fn main() {
    panic!("crash and burn");
}
let v = vec![1, 2, 3];
v[99];

Common Mistakes

  • Using panic! for recoverable errors
  • Not handling out-of-bounds access safely

Practice a panic:

let arr = [1, 2];
if arr.len() > 2 {
    panic!("Out of bounds");
}

Chapter 2: Recoverable Errors

Recoverable errors use Result to handle potential failures gracefully.

use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
}
let f = File::open("hello.txt").unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt");

Pro Tip

Use expect over unwrap for better error messages.

Practice recoverable errors:

use std::fs::File;
let f = File::open("test.txt").unwrap_or_else(|_| File::create("test.txt").unwrap());

Chapter 3: Propagating Errors

The ? operator simplifies error propagation in functions returning Result.

use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

Pro Tip

Use the ? operator to streamline error handling in Result-returning functions.

Practice error propagation:

use std::fs;
fn read_file() -> Result<String, std::io::Error> {
    fs::read_to_string("test.txt")
}
println!("{:?}", read_file());

Module 7: Generics, Traits, and Lifetimes

Chapter 1: Generics

Generics allow writing flexible, reusable code for multiple types.

fn largest(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
struct Point {
    x: T,
    y: T,
}
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };

Common Mistakes

  • Not constraining generic types with appropriate traits
  • Overcomplicating generic code when specific types suffice

Practice generics:

struct Pair { first: T, second: T }
let p = Pair { first: 1, second: 2 };
println!("first: {}, second: {}", p.first, p.second);

Chapter 2: Traits

Traits define shared behavior across types.

pub trait Summary {
    fn summarize(&self) -> String;
}
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Pro Tip

Use trait bounds to write flexible, reusable functions that accept any type implementing the trait.

Practice a trait:

trait Greet {
    fn greet(&self) -> String;
}
struct Person { name: String }
impl Greet for Person {
    fn greet(&self) -> String {
        format!("Hello, {}", self.name)
    }
}
let p = Person { name: String::from("Alice") };
println!("{}", p.greet());

Chapter 3: Lifetimes

Lifetimes ensure references remain valid.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
struct ImportantExcerpt<'a> {
    part: &'a str,
}
fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };
}

Pro Tip

Explicit lifetimes are often unnecessary due to Rust’s lifetime elision rules.

Practice lifetimes:

fn first_word<'a>(s: &'a str) -> &'a str {
    s.split(' ').next().unwrap()
}
let s = "hello world";
let word = first_word(s);
println!("{}", word);

Module 8: Advanced Features

Chapter 1: Smart Pointers

Smart pointers provide additional functionality beyond regular pointers.

let b = Box::new(5);
enum List {
    Cons(i32, Box<List>),
    Nil,
}
use std::rc::Rc;
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))));
use std::cell::RefCell;
let x = RefCell::new(42);
let y = x.borrow_mut();

Pro Tip

Use Rc and RefCell for shared mutable state in single-threaded contexts.

Practice smart pointers:

let x = Box::new(10);
println!("{}", *x);

Chapter 2: Concurrency

Rust’s ownership model ensures safe concurrency.

use std::thread;
use std::time::Duration;
let handle = thread::spawn(|| {
    for i in 1..10 {
        println!("hi number {} from the spawned thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
});
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    let val = String::from("hi");
    tx.send(val).unwrap();
});
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

Pro Tip

Use Arc and Mutex for safe shared mutable state across threads.

Practice concurrency:

use std::thread;
let handle = thread::spawn(|| {
    println!("Hello from thread!");
});
handle.join().unwrap();

Chapter 3: Advanced Traits and Types

Advanced trait features enable complex patterns.

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
trait AddSelf> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}
trait Pilot {
    fn fly(&self);
}
trait Wizard {
    fn fly(&self);
}
struct Human;
impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}
impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}
impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

Pro Tip

Use associated types in traits to reduce generic type parameters.

Practice advanced traits:

trait Speak {
    fn speak(&self);
}
struct Dog;
impl Speak for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}
let dog = Dog;
dog.speak();

Module 9: Macros and Unsafe Rust

Chapter 1: Macros

Macros enable metaprogramming by generating code at compile time.

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
let v: Vec<u32> = vec![1, 2, 3];
use proc_macro;
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

Pro Tip

Use macro_rules! for simple macros and procedural macros for complex code generation.

Practice a macro:

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}
say_hello!();

Chapter 2: Unsafe Rust

Unsafe Rust allows operations that bypass the borrow checker.

unsafe fn dangerous() {}
unsafe {
    dangerous();
}
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
    println!("r1 is: {}", *r1);
}
extern "C" {
    fn abs(input: i32) -> i32;
}
unsafe {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}

Common Mistakes

  • Using unsafe without understanding its risks
  • Dereferencing invalid raw pointers

Practice unsafe Rust:

let mut x = 10;
let ptr = &x as *const i32;
unsafe {
    println!("{}", *ptr);
}

Chapter 3: Advanced Lifetimes

Advanced lifetime techniques handle complex reference scenarios.

struct Context<'a>(&'a str);
struct Parser<'a> {
    context: &'a Context<'a>,
}
impl<'a> Parser<'a> {
    fn parse(&self) -> Result<&'a str, &'a str> {
        Ok(&self.context.0[1..])
    }
}
struct Ref<'a, T: 'a>(&'a T);
trait Red { }
struct Ball<'a> {
    diameter: &'a i32,
}
impl<'a> Red for Ball<'a> { }

Pro Tip

Use lifetime annotations sparingly, relying on elision where possible.

Practice advanced lifetimes:

fn shortest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() < y.len() { x } else { y }
}
println!("{}", shortest("hi", "hello"));

Module 10: Final Project

Chapter 1: Building a Multithreaded Web Server

Build a simple web server handling multiple connections.

use std::net::{TcpListener, TcpStream};
use std::io::prelude::*;
use std::thread;
fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}
fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();
    let get = b"GET / HTTP/1.1\r\n";
    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };
    let contents = std::fs::read_to_string(filename).unwrap();
    let response = format!("{}{}", status_line, contents);
    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Pro Tip

Use threads to handle concurrent connections efficiently.

Practice a simple server:

use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
println!("Server running");

Chapter 2: Optimizing with a Thread Pool

Improve the server by using a thread pool to limit the number of threads.

use std::sync::{mpsc, Arc, Mutex};
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

type Job = Box<dyn FnBox + Send + 'static>;

enum Message {
    NewJob(Job),
    Terminate,
}

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);
        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }
        ThreadPool { workers, sender }
    }

    pub fn execute(&self, f: F)
    where F: FnOnce() + Send + 'static
    {
        let job = Box::new(f);
        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }
        for worker in &mut self.workers {
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: OptionJoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<MutexReceiver<Message>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let message = receiver.lock().unwrap().recv().unwrap();
                match message {
                    Message::NewJob(job) => {
                        job.call_box();
                    },
                    Message::Terminate => {
                        break;
                    },
                }
            }
        });
        Worker { id, thread: Some(thread) }
    }
}

trait FnBox {
    fn call_box(self: Box<Self>);
}

implFnOnce()> FnBox for F {
    fn call_box(self: Box) {
        (*self)()
    }
}

Pro Tip

Use thread pools to manage resources in high-concurrency applications.

Practice a thread pool:

use std::sync::{mpsc, Arc, Mutex};
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send("hi").unwrap();
});
println!("{}", rx.recv().unwrap());

Chapter 3: Integrating the Thread Pool

Integrate the thread pool into the web server for efficient handling.

use std::net::{TcpListener, TcpStream};
use std::io::prelude::*;
use std::time::Duration;
use threadpool::ThreadPool;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();
    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";
    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };
    let contents = std::fs::read_to_string(filename).unwrap();
    let response = format!("{}{}", status_line, contents);
    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Pro Tip

Combine thread pools with async runtimes like Tokio for even better performance.

Practice integrating a thread pool:

use threadpool::ThreadPool;
let pool = ThreadPool::new(2);
pool.execute(|| {
    println!("Task executed");
});

Module 11: Introduction to Solana Blockchain Programming

Chapter 1: Solana Blockchain Overview

Solana is a high-performance blockchain with fast transaction speeds and low costs, using Rust for its programs due to its safety and performance.

// Example of a simple Solana program entrypoint
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    msg,
};

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
) -> ProgramResult {
    msg!("Hello, Solana!");
    Ok(())
}

Pro Tip

Use the solana_program crate to access Solana’s runtime and account structures.

Practice a basic Solana program:

use solana_program::{
    entrypoint,
    entrypoint::ProgramResult,
    msg,
};

entrypoint!(process_instruction);

fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8]
) -> ProgramResult {
    msg!("My first Solana program!");
    Ok(())
}

Chapter 2: Setting Up Solana Development Environment

Setting up a Solana development environment involves installing the Solana CLI, Rust, and Anchor framework for streamlined program development.

// Install Solana CLI (example command, not Rust code)
sh -c "$(curl -sSfL https://release.solana.com/v1.10.32/install)"

// Install Anchor (example command)
cargo install --git https://github.com/project-serum/anchor anchor-cli --locked

// Example Anchor program structure
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod my_program {
    use superpub fn initialize(ctx: Context) -> Result<()> {
        msg!("Initialized!");
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(init, payer = payer, space = 8)]
    pub data: Account<'info, Data>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct Data {}

Common Mistakes

  • Not specifying correct Solana CLI version
  • Missing Anchor dependencies in Cargo.toml
  • Incorrect program ID declaration

Practice setting up an Anchor program:

use anchor_lang::prelude::*;

declare_id!("11111111111111111111111111111111");

#[program]
pub mod hello {
    use super;
    pub fn say_hello(ctx: Context) -> Result<()> {
        msg!("Hello, Solana!");
        Ok(())
    }
}

#[derive(Accounts)]
pub struct SayHello<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
}

Chapter 3: Solana Program Structure

Solana programs use an entrypoint, process instructions, and manage accounts with specific constraints.

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
    msg,
};

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;
    if account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    msg!("Account data length: {}", account.data.borrow().len());
    Ok(())
}

Pro Tip

Always validate account ownership and data integrity to ensure program security.

Practice validating accounts:

use solana_program::{
    account_info::next_account_info,
    entrypoint::ProgramResult,
    program_error::ProgramError,
};

pub fn check_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account = next_account_info(&mut accounts.iter())?;
    if account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    Ok(())
}

Module 12: Building Solana Programs

Chapter 1: Instruction Processing

Solana programs process instructions by deserializing data and executing logic based on the instruction type.

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
    msg,
};
use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize)]
pub enum MyInstruction {
    Increment,
    Decrement,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Counter {
    count: u64,
}

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]
) -> ProgramResult {
    let instruction = MyInstruction::try_from_slice(instruction_data)?;
    let account = &accounts[0];
    if account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    let mut counter = Counter::try_from_slice(&account.data.borrow())?;
    match instruction {
        MyInstruction::Increment => counter.count += 1,
        MyInstruction::Decrement => counter.count -= 1,
    }
    counter.serialize(&mut &mut account.data.borrow_mut()[..])?;
    msg!("Counter value: {}", counter.count);
    Ok(())
}

Pro Tip

Use Borsh for efficient serialization/deserialization of instruction data.

Practice instruction processing:

use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Data {
    value: u32,
}

pub fn update_value(data: &[u8], account_data: &mut [u8]) -> Result<(), ProgramError> {
    let mut state = Data::try_from_slice(data)?;
    state.value += 1;
    state.serialize(account_data)?;
    Ok(())
}

Chapter 2: Account Management

Manage Solana accounts by initializing, updating, and validating account data.

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program_error::ProgramError,
    pubkey::Pubkey,
    program::invoke,
    system_instruction,
    rent::Rent,
    sysvar::Sysvar,
};
use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountData {
    value: u64,
}

pub fn initialize_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let data_account = next_account_info(account_info_iter)?;
    let payer = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let rent = Rent::get()?;
    let space = 8;
    let lamports = rent.minimum_balance(space);
    invoke(
        &system_instruction::create_account(
            payer.key,
            data_account.key,
            lamports,
            space as u64,
            program_id,
        ),
        &[payer.clone(), data_account.clone(), system_program.clone()],
    )?;
    let account_data = AccountData { value: 0 };
    account_data.serialize(&mut &mut data_account.data.borrow_mut()[..])?;
    Ok(())
}

Common Mistakes

  • Not allocating enough space for account data
  • Failing to validate rent exemption
  • Omitting system program in account creation

Practice initializing an account:

use solana_program::{
    account_info::AccountInfo,
    program_error::ProgramError,
};

pub fn init(account: &AccountInfo) -> Result<(), ProgramError> {
    let mut data = account.data.borrow_mut();
    data[0] = 0;
    Ok(())
}

Chapter 3: Cross-Program Invocation

Cross-program invocation (CPI) allows a Solana program to call another program.

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program::invoke,
    pubkey::Pubkey,
    system_instruction,
};

pub fn transfer_lamports(
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let from_account = next_account_info(account_info_iter)?;
    let to_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    invoke(
        &system_instruction::transfer(from_account.key, to_account.key, amount),
        &[
            from_account.clone(),
            to_account.clone(),
            system_program.clone(),
        ],
    )?;
    Ok(())
}

Pro Tip

Use CPI to leverage existing Solana programs like the System Program for common tasks.

Practice a CPI call:

use solana_program::{
    program::invoke,
    system_instruction,
    account_info::AccountInfo,
    entrypoint::ProgramResult,
};

pub fn send_lamports(
    from: &AccountInfo,
    to: &AccountInfo,
    amount: u64,
) -> ProgramResult {
    invoke(
        &system_instruction::transfer(from.key, to.key, amount),
        &[from.clone(), to.clone()],
    )?;
    Ok(())
}

Module 13: Advanced Solana Programming

Chapter 1: Program-Derived Addresses (PDAs)

Program-Derived Addresses (PDAs) are deterministic addresses controlled by a program, used for data storage.

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
    program::invoke_signed,
    system_instruction,
};

pub fn create_pda(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    seed: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let payer = next_account_info(account_info_iter)?;
    let pda = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let (pda_key, bump) = Pubkey::find_program_address(&[b"pda", seed], program_id);
    if pda_key != *pda.key {
        return Err(ProgramError::InvalidAccountData);
    }
    invoke_signed(
        &system_instruction::create_account(
            payer.key,
            &pda_key,
            1_000_000,
            8,
            program_id,
        ),
        &[payer.clone(), pda.clone(), system_program.clone()],
        &[b"pda", seed, &[bump]],
    )?;
    Ok(())
}

Pro Tip

Use PDAs to manage program-controlled accounts without storing private keys.

Practice creating a PDA:

use solana_program::{
    pubkey::Pubkey,
    program_error::ProgramError,
};

pub fn get_pda(
    program_id: &Pubkey,
    seed: &[u8],
) -> Result<Pubkey, ProgramError> {
    let (pda, _bump) = Pubkey::find_program_address(&[b"test", seed], program_id);
    Ok(pda)
}

Chapter 2: Error Handling in Solana Programs

Custom error types improve the robustness of Solana programs.

use solana_program::{
    program_error::ProgramError,
    msg,
};
use thiserror::Error;

#[derive(Error, Debug, Copy, Clone)]
#[repr(u32)]
pub enum CustomError {
    #[error("Insufficient funds")]
    InsufficientFunds = 0,
    #[error("Invalid instruction")]
    InvalidInstruction = 1,
}

impl From<CustomError> for ProgramError {
    fn from(e: CustomError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

pub fn check_funds(
    account: &AccountInfo,
    amount: u64,
) -> Result<(), ProgramError> {
    if account.lamports() < amount {
        msg!("Error: Insufficient funds");
        return Err(CustomError::InsufficientFunds.into());
    }
    Ok(())
}

Common Mistakes

  • Not converting custom errors to ProgramError
  • Ignoring error logging for debugging

Practice custom errors:

use solana_program::program_error::ProgramError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Invalid value")]
    InvalidValue,
}

pub fn check_value(value: u32) -> Result<(), ProgramError> {
    if value == 0 {
        return Err(ProgramError::Custom(0));
    }
    Ok(())
}

Chapter 3: Testing Solana Programs

Testing Solana programs involves simulating the runtime environment using tools like solana-program-test.

use solana_program_test::*;
use solana_sdk::{
    signature::Signer,
    transaction::Transaction,
    pubkey::Pubkey,
};
use my_program::instruction::MyInstruction;

#[tokio::test]
async fn test_increment() {
    let program_id = Pubkey::new_unique();
    let mut program_test = ProgramTest::new(
        "my_program",
        program_id,
        processor!(my_program::process_instruction),
    );
    let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
    let data_account = Pubkey::new_unique();
    let instruction = my_program::instruction::initialize(&program_id, &payer.pubkey(), &data_account);
    let mut transaction = Transaction::new_with_payer(
        &[instruction],
        Some(&payer.pubkey()),
    );
    transaction.sign(&[&payer], recent_blockhash);
    banks_client.process_transaction(transaction).await.unwrap();
}

Pro Tip

Use solana-program-test to simulate Solana’s runtime for robust unit tests.

Practice a simple test:

use solana_program_test::*;
use solana_sdk::signature::Signer;

#[tokio::test]
async fn test_program() {
    let program_id = Pubkey::new_unique();
    let mut program_test = ProgramTest::new(
        "test_program",
        program_id,
        None,
    );
    let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
}

Module 14: Building Solana dApps

Chapter 1: dApp Architecture

Decentralized applications (dApps) on Solana combine on-chain programs with off-chain clients, typically using JavaScript/TypeScript with libraries like @solana/web3.js.

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
};
use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize)]
pub struct VoteAccount {
    votes: u64,
}

pub fn vote(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    let account = &accounts[0];
    if account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    let mut vote_account = VoteAccount::try_from_slice(&account.data.borrow())?;
    vote_account.votes += amount;
    vote_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
    Ok(())
}

Pro Tip

Design dApps with clear separation between on-chain logic and off-chain interfaces for scalability.

Practice a voting dApp backend:

use solana_program::program_error::ProgramError;

pub fn increment_vote(data: &mut [u8]) -> Result<(), ProgramError> {
    let mut votes = u64::from_le_bytes(data.try_into().unwrap());
    votes += 1;
    data.copy_from_slice(&votes.to_le_bytes());
    Ok(())
}

Chapter 2: Client-Side Integration

Integrate Solana programs with client-side code using @solana/web3.js to send transactions and interact with accounts.

// JavaScript client code using @solana/web3.js
import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js';
import { Keypair } from '@solana/web3.js';

async function voteOnChain(programId, voteAccount, payer, amount) {
    const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
    const instruction = new TransactionInstruction({
        keys: [
            { pubkey: voteAccount, isSigner: false, isWritable: true },
            { pubkey: payer.publicKey, isSigner: true, isWritable: false },
        ],
        programId,
        data: Buffer.from([amount]),
    });
    const transaction = new Transaction().add(instruction);
    await sendAndConfirmTransaction(connection, transaction, [payer]);
}

// Example usage
const programId = new PublicKey('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS');
const voteAccount = new PublicKey('...'); // Replace with actual vote account public key
const payer = Keypair.generate();
voteOnChain(programId, voteAccount, payer, 1);

Pro Tip

Use @solana/web3.js for seamless interaction with Solana programs from the client side.

Practice client-side integration:

import { Connection, PublicKey } from '@solana/web3.js';

async function getBalance(publicKey) {
    const connection = new Connection('https://api.devnet.solana.com');
    const balance = await connection.getBalance(new PublicKey(publicKey));
    console.log(`Balance: ${balance} lamports`);
}

Chapter 3: Frontend Development

Build a frontend for a Solana dApp using React and @solana/web3.js to interact with the blockchain.

import React, { useState } from 'react';
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { useWallet } from '@solana/wallet-adapter-react';

function VoteButton({ programId, voteAccount }) {
    const { publicKey, sendTransaction } = useWallet();
    const [loading, setLoading] = useState(false);

    const handleVote = async () => {
        if (!publicKey) {
            alert('Wallet not connected!');
            return;
        }
        setLoading(true);
        try {
            const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
            const instruction = new TransactionInstruction({
                keys: [
                    { pubkey: voteAccount, isSigner: false, isWritable: true },
                    { pubkey: publicKey, isSigner: true, isWritable: false },
                ],
                programId,
                data: Buffer.from([1]),
            });
            const transaction = new Transaction().add(instruction);
            const signature = await sendTransaction(transaction, connection);
            await connection.confirmTransaction(signature, 'confirmed');
            alert('Vote successful!');
        } catch (error) {
            console.error(error);
            alert('Vote failed!');
        } finally {
            setLoading(false);
        }
    };

    return (
        
    );
}

Pro Tip

Use @solana/wallet-adapter-react for easy wallet integration in React dApps.

Practice a simple React component:

import React from 'react';

function ConnectButton() {
    return ;
}

Final Test

Test your knowledge with a comprehensive final exam covering all course modules.

Career Opportunities with Rust

Systems Programmer

Develop low-level software such as operating systems or embedded systems.

Average Salary: $110,000/year

Blockchain Developer

Build decentralized applications and smart contracts on blockchains like Solana.

Average Salary: $120,000/year

Game Developer

Create performant game engines and tools using Rust’s safety guarantees.

Average Salary: $100,000/year

Web Backend Developer

Develop high-performance web servers with frameworks like Actix or Warp.

Average Salary: $105,000/year

Airdrop Points: 0

© 2023 A Few Bad Newbies LLC. All rights reserved.