RustSystems ProgrammingMemory SafetyPerformance

Rust for Systems Programming: Memory Safety Without Garbage Collection

·Hilmall Cloud

Rust has become the language of choice for systems programming, offering memory safety without sacrificing performance. This guide covers the core concepts that make Rust unique and how to apply them in real-world systems code.

Ownership: Rust’s Secret Sauce

Rust’s ownership system ensures memory safety at compile time without a garbage collector. Every value has exactly one owner, and when the owner goes out of scope, the value is dropped.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is moved to s2
    
    // println!("{}", s1);  // ERROR: s1 is no longer valid
    println!("{}", s2);     // OK: s2 owns the string
}

Move Semantics

By default, Rust moves values rather than copying them:

struct Data {
    value: Vec<i32>,
}

fn process(data: Data) {
    // data is moved here
    println!("{:?}", data.value);
}

fn main() {
    let d = Data { value: vec![1, 2, 3] };
    process(d);
    // d is no longer valid here
}

Borrowing and References

Borrowing allows you to reference values without taking ownership:

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s goes out of scope, but since it doesn't own the value, nothing happens

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // Borrow s1
    println!("Length of '{}' is {}", s1, len);  // s1 still valid
}

Mutable References

Only one mutable reference is allowed at a time:

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

fn main() {
    let mut s = String::from("hello");
    modify(&mut s);
    println!("{}", s);  // "hello, world"
}

Lifetimes

Lifetimes ensure references are valid for as long as they’re used:

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

The 'a lifetime annotation says the returned reference will be valid as long as both input references are valid.

Error Handling

Rust uses Result and Option for explicit error handling:

use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file("config.txt") {
        Ok(contents) => println!("Read: {}", contents),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Concurrency

Rust’s ownership system prevents data races at compile time:

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        let val = String::from("hello");
        tx.send(val).unwrap();
    });
    
    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

Shared State

Use Arc (atomically reference counted) and Mutex for shared state:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    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);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Result: {}", *counter.lock().unwrap());
}

Performance Optimization

Zero-Cost Abstractions

Rust’s abstractions compile to the same code as hand-written C:

// Iterator chain - compiles to efficient loop
let sum: i32 = (1..1000)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .sum();

Inline Assembly

For maximum performance, use inline assembly:

use std::arch::asm;

unsafe fn atomic_add(ptr: *mut i32, val: i32) -> i32 {
    let old: i32;
    asm!(
        "lock xadd [{ptr}], {val}",
        ptr = in(reg) ptr,
        val = inout(reg) val => old,
        options(nostack)
    );
    old
}

Real-World Example: High-Performance TCP Server

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    
    loop {
        match stream.read(&mut buffer) {
            Ok(0) => break,  // Connection closed
            Ok(n) => {
                // Echo back
                stream.write_all(&buffer[..n]).unwrap();
            }
            Err(_) => break,
        }
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
    println!("Server listening on port 8080");
    
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| handle_client(stream));
            }
            Err(e) => eprintln!("Error: {}", e),
        }
    }
}

Working with Unsafe Code

Sometimes you need to step outside the borrow checker’s guarantees — interfacing with C libraries, implementing certain data structures, or hitting performance targets the safe abstractions can’t reach. Rust makes this explicit with unsafe blocks:

fn main() {
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 is: {}", *r1);  // Dereferencing raw pointer requires unsafe
        *r2 = 10;                     // Mutation through raw pointer
    }
}

The key discipline is keeping unsafe blocks small and wrapping them in safe abstractions. The standard library itself does this constantly — Vec and String contain plenty of unsafe internally, but expose safe interfaces. When you write unsafe, document the invariant you’re maintaining and why the borrow checker can’t verify it. This makes review possible and keeps the unsafe surface auditable.

The Cargo Ecosystem

Cargo is more than a package manager — it’s the build system, test runner, and documentation generator in one:

cargo new myproject        # Scaffold a new project
cargo build --release      # Optimized build
cargo test                 # Run tests
cargo clippy               # Lint for common mistakes
cargo fmt                  # Format code
cargo doc --open           # Generate and open documentation
cargo audit                # Check dependencies for known vulnerabilities

Add cargo clippy and cargo fmt to CI from day one. Clippy catches hundreds of common mistakes — from redundant clones to suspicious operator precedence — and the fixes it suggests are usually the idiomatic ones. The Cargo.toml manifest declares dependencies with semver ranges, and the Cargo.lock file pins exact versions for reproducible builds. Commit the lockfile for applications, not for libraries.

Traits and Generics

Traits are Rust’s mechanism for shared behavior — closer to typeclasses than to classical inheritance:

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

    fn summarize_default(&self) -> String {
        String::from("(Read more...)")
    }
}

fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Generics with trait bounds compile to monomorphized code — a specialized copy for each concrete type — so there’s no runtime dispatch cost. Use dynamic dispatch with &dyn Trait only when you genuinely need heterogeneous collections.

When Not to Use Rust

Honesty matters: Rust is not the right tool for every job. The borrow checker has a learning curve that slows down prototyping, and for a quick script or a CRUD web app, the safety guarantees buy you little while costing development speed. Rust shines when correctness and performance justify the upfront investment — CLI tools, network services, embedded systems, and anything that would otherwise be written in C or C++. If you’re building a microservice in a larger polyglot system, our Go microservices guide covers a gentler on-ramp.

Conclusion

Rust’s ownership model provides memory safety without runtime overhead. The learning curve is steep, but the payoff is systems code that’s both fast and safe.

Start with small projects to internalize ownership and borrowing. The compiler’s error messages are excellent teachers — read them carefully and they’ll guide you to correct, efficient code.

For systems programming where safety and performance matter, Rust is increasingly the obvious choice.