Solana Fellowship Week 2: Things Got Real with Rust
2026-04-02 (10 min read)
follow on x.com for more updates
api.fetch_data(total_visitors);
Week 2 is where things started getting tough. The concurrency (multi-threading) part honestly blasted my brain at first, and I felt like I was understanding nothing. But once I slowed down and started breaking each topic into smaller pieces, things got way clearer one by one. This post is that journey: smart pointers, trait objects, macros, and multithreading patterns that looked scary first and then started making sense.
Smart pointers are pointers, but with extra capabilities and guarantees.
Box<T> stores data on heap instead of stack.
fn main() {
let x: Box<i32> = Box::new(30); // heap data
let y: i32 = 30; // stack data
println!("{} {}", *x, y);
}Memory layout idea:
Recursive types:
enum List {
Cons(i32, Box<List>),
Nil,
}Large data (avoid huge stack allocations):
fn main() {
let big_array = Box::new([0_i32; 1_000_000]);
println!("{}", big_array[0]);
}Trait objects (dynamic dispatch):
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
r: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.
Recursive list traversal:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn list_sum(list: &List) -> i32 {
let mut sum = 0;
let mut cur = list;
loop {
match cur {
List::Cons(v
Deref lets custom types behave like references when using *.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main
Rust can auto-convert reference types when possible:
&T -> &U if T: Deref<Target = U>&mut T -> &mut U if T: DerefMut<Target = U>&mut T -> &Tfn hello(s: &str) {
println!("hello, {}", s);
}
fn main() {
let name: Box<String> = Box::new(String::from("nycx"));
hello(&name); // &Box<String> -> &String -> &str
}Drop customizes cleanup when value goes out of scope.
struct MySmartPointer {
data: String,
}
impl Drop for MySmartPointer {
fn drop(&mut self) {
println!("dropping: {}", self.data);
}
}
fn main() {
let m = MySmartPointer {
data: String::from
Rc<T> enables multiple owners of heap data in single-threaded code.
use std::rc::Rc;
fn main() {
let shared = Rc::new(String::from("hello"));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
println!("{} {} {}", shared,
Rc::clone does not deep copy inner data; it increments ref count.
RefCell<T> does borrow-checking at runtime, not compile time.
use std::cell::RefCell;
fn main() {
let x = RefCell::new(32);
*x.borrow_mut() = 12;
println!("{}", x.borrow());
}Counter example:
use std::cell::RefCell;
struct Counter {
value: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Self {
value: RefCell::new(0),
}
}
fn increment(&
Good when collection holds different concrete types that share behavior.
use std::f64::consts::PI;
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rect {
w: f64,
h: f64,
}
trait Formatter {
fn format(&self, input: &str) -> String;
}
struct Upper;
struct Snake;
struct Trim;
impl Formatter for Upper {
fn format(&self, input: &str
You cannot implement a foreign trait on a foreign type directly, but you can wrap the type.
use std::fmt::{self, Display};
struct CommaSeparated(Vec<i32>);
impl Display for CommaSeparated {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = self
trait Summary {
type Output;
fn summarize(&self) -> Self::Output;
}
struct Numbers {
data: Vec<i32>,
}
struct Words {
data: Vec<String>,
}
impl Summary for Numbers {
use std::fmt::{self, Display};
use std::ops::Add;
struct Vec2 {
x: f64,
y: f64,
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self
Functions have concrete type fn(Args) -> Return.
fn double(x: i32) -> i32 {
x * 2
}
fn increment(x: i32) -> i32 {
x + 1
}
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x
Closures are unnamed types; box trait objects when returning from functions.
fn make_multiplier(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x * n)
}
fn compose(
f: Box<dyn Fn(i32) -> i32>,
g: Box<dyn Fn(i32
Use guards for extra runtime conditions and @ to bind matched values.
fn classify(n: i32) -> String {
match n {
0 => "zero".to_string(),
x @ 1..=10 => format!("small: {}", x),
x @ -10..=-1 => format!("neg small: {}", x),
x if x
fn parse_command(input: &str) -> String {
let tokens: Vec<&str> = input.split_whitespace().collect();
match tokens.as_slice() {
["quit"] => "Goodbye".to_string
Raw pointers bypass borrow rules, so dereferencing must happen inside unsafe.
fn swap_values(a: &mut i32, b: &mut i32) {
let ap: *mut i32 = a;
let bp: *mut i32 = b;
unsafe {
std::ptr::swap(ap, bp);
}
}Wrap unsafe internals in safe public methods.
struct SafeArray {
data: Vec<i32>,
}
impl SafeArray {
fn new(data: Vec<i32>) -> Self {
Self { data }
}
fn get(&self, i: usize) -> Option<
macro_rules! matches patterns and expands into code.
macro_rules! square {
($a:expr) => {
$a * $a
};
}
fn compute(n: i32) -> i32 {
square!(n)
}Multiple arms:
macro_rules! convert {
(celsius_to_f, $a:expr) => {
($a * 9 / 5) + 32
};
(f_to_celsius, $a:expr) => {
($a - 32) * 5 / 9
};
}
fn temp_test(c: i32
Macro repetition:
macro_rules! sum {
() => {
0
};
($($a:expr),+ $(,)?) => {{
let mut total = 0;
$(total += $a;)*
total
}};
}
fn total(a: i32, b: i32,
use std::thread;
fn parallel_sum(nums: Vec<i32>) -> i32 {
let mid = nums.len() / 2;
let v1 = nums[..mid].to_vec();
let v2 = nums[mid..].to_vec();
let
use std::sync::mpsc;
use std::thread;
fn process_values(values: Vec<i32>) -> Vec<i32> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for v in values {
tx
Mutex<T> ensures one thread accesses data at a time.
use std::sync::Mutex;
fn main() {
let x = Mutex::new(5);
{
let mut guard = x.lock().unwrap();
*guard = 10;
}
println!("{:?}", x);
}Arc<T> gives shared ownership across threads, Mutex<T> gives safe mutation.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
use std::sync::mpsc;
use std::thread;
fn pipeline(input: Vec<i32>) -> Vec<String> {
let (tx1, rx1) = mpsc::channel();
thread::spawn(move || {
That’s week 2 in one place. Smart pointers and concurrency looked intimidating at first glance, but once I started seeing the same patterns repeat (Box, Rc, RefCell, Arc, Mutex, trait objects), things became much clearer. Next step is just writing more small programs and using these patterns in actual projects.