Getting Started: The Rust Playground

Before writing local code, use the Rust Playground at play.rust-lang.org. Paste any example from this lesson and run it in your browser. No install needed.


Variables and Mutability

In most languages, variables can be changed after you set them. In Rust, they cannot unless you say so explicitly.

let x = 5;          // x is 5 and stays 5 forever
let mut y = 5;      // y starts at 5
y = 15;             // y is now 15 — allowed because of mut

If you try to change an immutable variable, the compiler stops you before your code even runs.

Constants are a stronger form of immutable. They require an explicit type and cannot be set to a value computed at runtime:

const MAX_POINTS: u32 = 100_000;

Data Types

Rust needs to know the type of every value at compile time. Usually it figures this out on its own, but you can always be explicit.

Integers

let amount: u64 = 1_000_000;   // SOL amount in lamports
let delta: i64 = -500;          // a change that could go negative

Floats

Booleans

let is_active: bool = true;

Characters

let letter: char = 'A';    // single quotes for char, double for strings

Arrays — fixed size, all same type

let arr: [i32; 3] = [1, 2, 3];  // exactly 3 signed 32-bit integers

Tuples — fixed size, can mix types

let t: (i32, f64, bool) = (42, 3.14, true);
let first = t.0;    // access by index

Strings

Two kinds of strings in Rust. They behave differently and are used for different things.

String: owned, growable, stored on the heap

let mut s = String::from("hello");
s.push_str(", world");    // s is now "hello, world"

&str: a borrowed string slice — a read-only window into existing string data

let s = String::from("hello world");
let hello: &str = &s[0..5];    // "hello" — a view, not a copy

When to use which:


Control Flow

If / else

let x = 10;
if x > 5 {
    println!("big");
} else {
    println!("small");
}

// You can assign from an if expression
let result = if x > 5 { "big" } else { "small" };

Loops

// loop — runs forever until break
loop {
    if done { break; }
}

// while
let mut n = 3;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

// for — iterating a range or collection
for i in 0..5 {
    println!("{}", i);    // prints 0, 1, 2, 3, 4
}

Match — like a switch but must cover every case

let number = 3;
match number {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("something else"),    // _ is the wildcard
}

Match is used constantly in Rust for working with Result, Option, enums, and error codes.


Functions

fn add(a: i32, b: i32) -> i32 {
    return a + b;        // explicit return
}

fn square(x: i32) -> i32 {
    x * x               // implicit return — no semicolon = return this value
}

let result1 = add(5, 3);      // 8
let result2 = square(5);      // 25

Rules:


Structs

Structs group related fields into one named type.

struct User {
    username: String,
    email: String,
    active: bool,
}

// Create an instance
let user1 = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    active: true,
};

println!("{}", user1.username);    // "alice"

Structs can contain other structs and enums. In Anchor, your on-chain account data is always a struct.


Enums

Enums define a type that can be exactly one of several named variants. Each variant can carry different data.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
    Unknown,
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
let nowhere = IpAddr::Unknown;

The power of enums is that match forces you to handle every variant:

match home {
    IpAddr::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
    IpAddr::V6(s) => println!("{}", s),
    IpAddr::Unknown => println!("no address"),
}

Ownership

This is what makes Rust different. Three rules:

  1. Every value has one owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped
let s3 = String::from("block");
{
    let s4 = s3;             // s3 is moved to s4 — s3 no longer valid
    println!("{}", s4);      // fine
}                            // s4 goes out of scope — string is dropped
println!("{}", s3);          // COMPILER ERROR — s3 was moved

The compiler tracks ownership. When a value is dropped, Rust frees the memory with no garbage collector needed.

Copy types (integers, booleans, floats) are copied automatically, not moved. The move only happens with heap-allocated types like String and Vec.


References and Borrowing

References let you use a value without taking ownership. You borrow it, use it, give it back.

Immutable reference — read-only

fn calculate_length(s: &String) -> usize {
    s.len()    // can read s, but not change it
}

let s1 = String::from("hello");
let len = calculate_length(&s1);    // pass a reference with &
// s1 is still valid here

Mutable reference — read and write

fn change(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
change(&mut s);

The borrowing rules:

These rules are enforced at compile time. They prevent data races before your code runs.


Traits

Traits define behavior that types can share. Like an interface in other languages.

trait Summary {
    fn summarize(&self) -> String;
}

struct Article {
    headline: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}", &self.content[..50])
    }
}

&self means the function gets a reference to the struct. It can read the fields but the struct keeps ownership.

Traits can have default implementations. If you do not override a function, the default runs.


Lifetimes

Lifetimes tell the compiler how long a reference must stay valid. They prevent you from using a reference after the data it points to has been dropped.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a is a lifetime annotation. It says the returned reference lives as long as the shorter of x and y.

You will see lifetimes constantly in Anchor: Account<'info, T>, Context<'_, '_, '_, 'info, T>. You rarely write them yourself, but you must be able to read them.


Error Handling

Result<T, E>: a value that is either success or failure

fn divide(a: f64, b: f64) -> Result {
    if b == 0.0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

match divide(10.0, 2.0) {
    Ok(result) => println!("Result: {}", result),
    Err(e) => println!("Error: {}", e),
}

Option<T>: a value that may or may not exist

fn find_char(c: char, s: &str) -> Option {
    for (i, ch) in s.chars().enumerate() {
        if ch == c { return Some(i); }
    }
    None
}

Use Result when something can fail with an error. Use Option when something can be absent without that being an error.


Project Structure

my-project/
  Cargo.toml          ← package manifest: name, version, dependencies
  src/
    main.rs           ← binary crate entry point
    lib.rs            ← library crate entry point (used by Anchor)
    my_module.rs      ← a module
cargo new my-project          # create a new binary crate
cargo new my-library --lib    # create a library crate
cargo run                     # compile and run

Modules are declared with mod and can be public or private:

pub mod instructions;    // public module in its own file
mod helpers;             // private module

Every Anchor program is a library crate. lib.rs is the entry point.