LabHub

Blog

The Return of Rust and Systems Programming — Ownership, Borrowing, Lifetime, Trait, Async, Tokio, Cargo, WebAssembly (2025)

한국어English日本語

"In Rust, we don't trust the programmer. We trust the compiler — and the programmer, trained by the compiler, becomes trustworthy." — Niko Matsakis (Rust core team)

In February 2024, the White House officially recommended migration from C/C++ to memory-safe languages. In February 2025, the Linux kernel merged Rust drivers into its main tree. Nine years at the top of Stack Overflow's "most loved language" survey. This is not fandom — it signals a structural shift across the entire software industry.

Despite Rust's reputation for difficulty, why is it this influential? This post answers what it means to "understand Rust," from its origin to the 2025 ecosystem map.


1. The Birth of Rust — Mozilla's Gift

Graydon Hoare's personal project (2006)

Mozilla official project (2010)

Servo to Firefox Quantum (2017)

Foundation independence (2021)

2025 — ubiquity


2. Ownership — Memory Safety Without GC

The problem

Rust's three rules

  1. Each value has exactly one owner
  2. Values are dropped when the owner goes out of scope
  3. Once the owner changes, the previous owner cannot be used

Basic example

fn main() {
    let s = String::from("hello");
    let t = s;  // ownership moves from s to t
    // println!("{}", s);  // error! s is no longer valid
    println!("{}", t);  // OK
}  // t goes out of scope -> drop

Copy vs Move

let x = 5;
let y = x;  // Copy
println!("{} {}", x, y);  // OK

let s1 = String::from("hi");
let s2 = s1;  // Move
// println!("{}", s1);  // error!

Clone — explicit copy

let s1 = String::from("hi");
let s2 = s1.clone();  // copies the heap
println!("{} {}", s1, s2);  // OK

.clone() makes the cost visible in the code. No implicit performance hit.


3. Borrowing — The Art of Lending

Why it is needed

fn print_len(s: String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(s);  // ownership moved
// s cannot be used here

Inconvenient. Hence references &.

Immutable borrow

fn print_len(s: &String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(&s);
println!("{}", s);  // OK, ownership kept

Mutable borrow

fn add_world(s: &mut String) {
    s.push_str(" world")
}

let mut s = String::from("hi");
add_world(&mut s);
println!("{}", s);  // "hi world"

Core rule — "shared XOR mutable"

At any moment:

let mut s = String::from("hi");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;  // error! immutable borrows already exist

Why this rule


4. Lifetime — How Long a Reference Lives

The problem

fn dangling() -> &String {
    let s = String::from("hi");
    &s
}  // s is dropped; &s would dangle

Segfault in C, compile error in Rust.

Lifetime annotations

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

'a is a shared lifetime inside the function. The return value lives as long as the shorter of x and y.

Lifetime elision

Most cases do not require annotation:

fn first_word(s: &str) -> &str { /* ... */ }
// internally: fn first_word<'a>(s: &'a str) -> &'a str

Three rules:

  1. Each reference parameter gets its own lifetime
  2. With one parameter, the return lifetime equals that parameter
  3. With &self, the return lifetime equals self's

'static

let s: &'static str = "hello";  // string literal

Lives for the entire program. Often abused.

Why lifetime is hard


5. Trait System — Rust's Core Abstraction

Trait equals Interface plus Mixin plus Typeclass

trait Greet {
    fn hello(&self) -> String;
    fn goodbye(&self) -> String {  // default
        String::from("bye")
    }
}

struct Kim { name: String }

impl Greet for Kim {
    fn hello(&self) -> String {
        format!("Hi, I am {}", self.name)
    }
}

Generic with trait bounds

fn greet_all<T: Greet>(people: &[T]) {
    for p in people {
        println!("{}", p.hello());
    }
}

T: Greet means T is any type that implements Greet. Similar to Java generics upper bounds but more powerful.

impl Trait (2018)

fn make_greeter() -> impl Greet {
    Kim { name: String::from("Kim") }
}

"Returns some Greet implementor" — hides the concrete type.

Trait objects — dyn Trait

fn greet_all(people: &[Box<dyn Greet>]) {
    for p in people {
        println!("{}", p.hello());
    }
}

vtable-based dynamic dispatch. Similar to C++ virtual functions, but the dyn keyword makes the cost explicit.

Orphan rule

You can only impl when either the trait or the type is owned by the current crate. Prevents conflicting implementations from different crates.

Famous traits


6. Zero-cost Abstractions — Bjarne's Promise, Rust's Delivery

Philosophy

"What you don't use, you don't pay for. What you do use, you couldn't hand-code better." — Bjarne Stroustrup

Example 1 — Iterator

let sum: i32 = (1..=100)
    .filter(|n| n % 2 == 0)
    .map(|n| n * 2)
    .sum();

High-level functional chain compiles to assembly identical to a hand-written for-loop. LLVM optimization erases the abstraction entirely.

Example 2 — Option<T>

let x: Option<i32> = Some(5);
let y = x.unwrap_or(0);

Safer than C null checks but has zero runtime cost. Option compiles to a tagged union.

Example 3 — Generic vs dyn Trait

// Generic — monomorphized at compile time
fn static_greet<T: Greet>(x: T) { /* ... */ }  // inlinable

// Trait object — runtime vtable
fn dynamic_greet(x: &dyn Greet) { /* ... */ }  // pointer indirection

You choose the cost.


7. Error Handling — Result, Not panic

Result<T, E>

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

match divide(10, 2) {
    Ok(v) => println!("{}", v),
    Err(e) => eprintln!("{}", e),
}

The ? operator — error propagation

fn do_stuff() -> Result<(), MyError> {
    let data = read_file("x.txt")?;
    let parsed = parse(&data)?;
    save(parsed)?;
    Ok(())
}

Clean propagation without try/catch. The caller must return Result.

panic! — only for exceptional cases

thiserror and anyhow

use anyhow::{Result, Context};

fn load_config() -> Result<Config> {
    let s = std::fs::read_to_string("config.yml")
        .context("failed to read config.yml")?;
    Ok(serde_yaml::from_str(&s)?)
}

8. Async/Await and Tokio

async/await basics

async fn fetch_user(id: u64) -> Result<User> {
    let resp = reqwest::get(format!("/users/{}", id)).await?;
    Ok(resp.json().await?)
}

#[tokio::main]
async fn main() {
    let user = fetch_user(1).await.unwrap();
    println!("{:?}", user);
}

async produces a Future

Calling an async fn alone does nothing — it returns a Future. A runtime must poll it to execute.

Runtimes

Tokio strengths

Difference from JS async/await

AspectJSRust
Default executionstarts immediately (Promise)nothing happens until awaited
Runtimebuilt-inlibrary
TypePromise<T>impl Future<Output=T>
Cancellationimplicit, trickydrop cancels automatically
PerformanceV8 tuning limitsnative

Async trait — finally in 2024

async fn in trait required workarounds for years. Rust 1.75 (Dec 2023) began stabilizing it; finalized in 2024-2025.


9. Cargo — The Package Manager Ideal

One tool for everything

cargo new myapp          # new project
cargo build              # build
cargo run                # run
cargo test               # tests
cargo bench              # benchmarks
cargo doc                # docs
cargo fmt                # formatting (rustfmt)
cargo clippy             # lint
cargo publish            # upload to crates.io

Dependencies, build, test, docs, benchmarks — one standard command each. Stark contrast to Python/Node fragmentation (pip/poetry, npm/yarn/pnpm).

Cargo.toml

[package]
name = "myapp"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"

[dev-dependencies]
criterion = "0.5"

Features — optional capabilities

[features]
default = ["tokio"]
tokio = ["dep:tokio"]
async-std = ["dep:async-std"]

Supports multiple runtimes within the same crate, combined with conditional compilation.

Workspace — built-in monorepo

[workspace]
members = ["crates/*"]

Multiple crates in one repo, unified dependency versions.

crates.io


10. Famous Rust Projects 2025

Infrastructure

Containers / cloud

Frontend tooling

Runtime / DB

Web / server


11. Rust on the Frontend — WebAssembly

wasm-bindgen

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

Callable from JS as greet("Kim").

Leptos — Solid.js style

#[component]
fn Counter() -> impl IntoView {
    let (n, set_n) = create_signal(0);
    view! {
        <button on:click=move |_| set_n.update(|v| *v += 1)>
            "count: " {n}
        </button>
    }
}

Dioxus — React style

fn Counter(cx: Scope) -> Element {
    let n = use_state(cx, || 0);
    render! {
        button { onclick: move |_| n.set(n + 1), "{n}" }
    }
}

Yew

Reality


12. Rust for Linux — Victory in 2025

The long road

Impact

Tensions

Asahi Linux team


13. Government and Regulation — Rust Mandated

White House ONCD report (Feb 2024)

"Memory-safe languages, including Rust, Go, C#, Java, Swift, Python, and JavaScript, offer...the most comprehensive approach to eliminate an entire class of vulnerabilities."

DoD, NASA, aviation

Ferrocene — certified Rust compiler

2025 today


14. Fastest Path to Learn Rust

Stage 1 (1-2 weeks)

  1. Finish The Rust Book (free, official)
  2. 100 rustlings exercises
  3. Build a small CLI (argh/clap)

Stage 2 (2-4 weeks)

  1. Get comfortable with Ownership and Borrowing
  2. Internalize Result and ?
  3. Master Iterator chains
  4. Practice Trait design

Stage 3 (1-3 months)

  1. Network apps with Async/Tokio
  2. JSON/YAML with serde
  3. Error type design (thiserror/anyhow)
  4. Ship a small real project

Stage 4 (3+ months)

  1. Understand unsafe
  2. Macros (declarative / procedural)
  3. Advanced lifetimes
  4. FFI (C interop)
  5. Contribute to open source crates

Free resources


15. Top 10 Anti-patterns

  1. Over-using .clone() — compiles, but wastes performance
  2. .unwrap() everywhere — panics in production
  3. Avoiding borrow rules with Rc<RefCell<T>> — giving up Rust's strengths
  4. Async for everything — CPU-bound code does not need it
  5. Over-specifying lifetimes — elision handles most
  6. dyn Trait without considering cost — generics often faster
  7. Macro overuse — compile-time blowup, debugger hell
  8. Box<dyn Error> overuse — losing fine-grained error types
  9. unsafe when you do not need it — forfeiting safety
  10. Non-Rust paradigms — stick to snake_case and trait-centric design

16. Rust Wisdom Checklist


Closing — "The Renaissance of Systems Programming"

For 30 years, systems programming was a C/C++ monopoly. Few believed it could be broken. Even when Rust 1.0 shipped in 2015, most experts dismissed it as a curiosity.

A decade later, the Linux kernel, the White House, Azure, and AWS are all moving to Rust. This resembles generational change more than revolution. C will not vanish, but new system code is increasingly born in Rust.

Rust's real gift is not speed. It is the experience of collaborating with the compiler to write correct programs. Fighting the borrow checker trains your intuition for memory, concurrency, and lifetimes — and that intuition travels with you into any language.

"Writing Rust is sometimes like being shouted at by an expert compiler. When you finally agree with it, your code is bulletproof." — Amos Wenger (fasterthanli.me)


"Rust is for when you need to be fast. Go is for when you need to be done. Python is for when you need both. TypeScript is for when you had a Python and want to sleep at night." — Anonymous HN comment (2024)

Comments

No comments yet.

Sign in to leave a comment