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
unsafewithout 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.