Rust primer (for Solana, in 10 minutes)

⏱ 10 min · 🎯 you'll understand: just enough Rust to read and edit Anchor programs.

You don't need to be a Rust expert to use sunscreen. You need to read code generated by scaffold and tweak it. This primer gives you exactly that.

The 30-second mental model

Rust is statically typed, has no garbage collector, and tracks who owns each value at compile time. In Anchor programs, the heavy machinery is hidden behind macros — #[program], #[derive(Accounts)], #[account]. You mostly write straight-line code inside instruction handlers.

Ownership (in 1 paragraph)

Every value has one owner. When the owner goes out of scope, the value is dropped (memory freed). You can lend a value out with &value (read-only) or &mut value (read-write). At any moment, you can have either many read-only borrows or one mutable borrow. The compiler enforces this — get used to error messages naming "borrow checker". For Anchor programs, this almost never bites you, because the framework manages account data.

Structs and impls

#![allow(unused)]
fn main() {
pub struct Counter {
    pub count: u64,
}

impl Counter {
    pub fn increment(&mut self) {
        self.count += 1;
    }
}
}

A struct holds fields. An impl block defines methods. &mut self means "this method can modify the struct".

Anchor's three macros

This is 90% of what you'll see in scaffolded code.

#[program]

#![allow(unused)]
fn main() {
#[program]
pub mod my_app {
    use super::*;

    pub fn greet(ctx: Context<Greet>, name: String) -> Result<()> {
        msg!("Hello, {}!", name);
        Ok(())
    }
}
}

The #[program] macro turns each pub fn into a callable instruction. Context<Greet> carries the accounts the instruction needs (declared next). Result<()> is Rust's standard error type — Ok(()) means success with no return value.

#[derive(Accounts)]

#![allow(unused)]
fn main() {
#[derive(Accounts)]
pub struct Greet<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
}

This declares which accounts the instruction takes. mut = will be modified. Signer = must sign the transaction. The 'info is a lifetime — think of it as "these accounts live for the duration of this call". You'll rarely touch lifetimes manually.

#[account]

#![allow(unused)]
fn main() {
#[account]
pub struct Counter {
    pub count: u64,
    pub authority: Pubkey,
}
}

#[account] marks a struct as on-chain storage. Anchor handles serialization/deserialization, plus a discriminator prefix.

Common types you'll see

TypeWhat it is
u8 .. u64, i8 .. i64unsigned/signed integers
booltrue / false
Stringheap-allocated UTF-8 string
Vec<T>heap-allocated growable array of T
Pubkey32-byte Solana public key
Option<T>Some(T) or None
Result<T, E>Ok(T) or Err(E)

Error handling

In Anchor handlers you'll see:

#![allow(unused)]
fn main() {
require!(amount > 0, MyError::AmountZero);
some_account.field = value;
Ok(())
}

require! is a macro: if the condition is false, it returns Err(MyError::AmountZero). Otherwise execution continues. Ok(()) returns success.

The ? operator

#![allow(unused)]
fn main() {
let data = account.try_borrow_data()?;
}

The ? says: if the call returned Err, propagate it up; if Ok, unwrap the value and continue. It's a shortcut for match.

What you can safely ignore (for now)

  • Traits and generics beyond what Anchor uses.
  • Async/await (Anchor programs are synchronous).
  • Smart pointers (Box, Rc, Arc) — Anchor doesn't need them in handlers.
  • Macros internals — just use them.

Going deeper

When you're ready: https://doc.rust-lang.org/book/ is the canonical, well-paced book. The chapters that matter for Anchor:

  • Ch. 4: ownership + borrowing.
  • Ch. 5–6: structs and enums.
  • Ch. 9: error handling.
  • Ch. 13: closures and iterators (useful for client code).

Next