jongkwan.dev
Development · Essay №020

Rust Overview: A Systems Language That Combines Safety and Performance

A complete tour of Rust's core concepts: ownership, borrowing, lifetimes, traits, and async/await

Jongkwan Lee2025년 10월 11일30 min read
Contents

Rust is a systems language that gets memory safety without a garbage collector and C/C++ level performance at the same time, using compile-time ownership checks.

Rust is a systems programming language that guarantees memory safety at compile time while delivering performance on par with C and C++. That guarantee rests on three mechanisms: the ownership system, the borrow checker, and lifetimes.

Language overview

Rust began in 2006 as Graydon Hoare's personal project, picked up Mozilla sponsorship in 2009, and was announced publicly in 2010. The stable 1.0 release arrived in 2015. The goal was a systems programming language that satisfies safety, performance, and concurrency all at once.

Rust targets the same domain as C and C++. Its ownership and borrowing model, plus strict compiler checks at build time, eliminate a large class of runtime errors such as null pointer dereferences and data races.

Three characteristics summarize the language.

  1. Safety
    Rust performs many checks at compile time, including ownership, lifetime, and data race checks, so memory errors and concurrency bugs never reach runtime.
  2. Performance
    As a systems language, Rust delivers performance comparable to C and C++. It uses no garbage collector, and low-level memory control is available when needed.
  3. Concurrency
    Ownership and thread-safety rules make concurrent programming both safe and efficient.

Development environment

Installing Rust and configuring an editor is a one-time cost that pays off in every session afterward.

  • Installing Rust

    • rustup is the usual route, and it installs both the Rust compiler (rustc) and the package manager (cargo) in one step.
    • Windows, macOS, and Linux all install by running a single script.
    • After installation, rustc --version and cargo --version confirm the installed versions.
  • Choosing an IDE or editor

    • Visual Studio Code
      • Installing a Rust extension such as "rust-analyzer" provides autocompletion, linting, and debugging.
    • IntelliJ IDEA / CLion
      • The JetBrains Rust plugin offers comparable code insight and lint features.
    • Plugins also exist for Vim, Emacs, Neovim, and other editors.

Core tools: rustc and cargo

A single file compiles directly with rustc, while real projects are managed with cargo.

  • rustc

    • The Rust compiler, which can compile a single file directly:

      bash
      rustc main.rs
      ./main
  • cargo

    • The package manager and build system of the Rust ecosystem.
    • Create a project: cargo new project-name
    • Build: cargo build (debug build)
    • Run: cargo run (builds, then runs)
    • Release build: cargo build --release (optimized build)
    • Dependency management: declare crate dependencies in Cargo.toml, and cargo build downloads and builds them automatically.

Program structure

A Rust program is organized around an entry point function, modules, and file layout.

  • The main function

    • As in C-family languages, main is the program entry point.

    • Example:

      rust
      fn main() {
          println!("Hello, Rust!");
      }
  • Module structure

    • The mod keyword defines a module and use brings items into scope.
    • As a project grows, splitting it across several files forms a module tree.
  • File layout

    • When main.rs sits in the src directory, cargo run treats that file as the entry point.
    • A library crate uses lib.rs instead of main.rs.

Basic types

Rust provides integer and floating-point types at explicit bit widths, and splits strings into slices and owned strings.

  • Integers
    • Signed: i8, i16, i32, i64, i128, isize
    • Unsigned: u8, u16, u32, u64, u128, usize
    • The default is i32
  • Floating point
    • f32, f64 (the default is f64)
  • Boolean
    • bool, either true or false
  • Character
    • char, a Unicode scalar value (for example 'a', '한', '😊')
  • Strings
    • &str: a string slice (immutable, referenced from the stack)
    • String: heap-allocated and mutable

Variables and mutability

Variables are immutable by default; mutability and constants are declared explicitly.

  • let

    • Declares a variable, immutable by default.

    • Example:

      rust
      let x = 5; // x is immutable
  • mut

    • Adding mut before the name makes the variable mutable.

    • Example:

      rust
      let mut y = 10;
      y = 20; // the value can be changed
  • Constants (const)

    • Defines a value determined at compile time rather than runtime.

    • A type annotation is required.

    • Example:

      rust
      const MAX_POINTS: u32 = 100_000;

Functions and scope

Functions are declared with fn, and values inside a block scope are released when the block ends.

  • Defining a function

    • Functions are declared with the fn keyword.

    • Parameter types must be written out.

    • Example:

      rust
      fn add(a: i32, b: i32) -> i32 {
          a + b // without a semicolon, this expression is the return value
      }
  • Return values

    • A function returns either its last expression or a value given to return.
  • Scope

    • A pair of braces {} forms one scope.
    • When the scope ends, variables created inside it are dropped from memory, per the ownership rules.

Ownership rules

Memory safety, the central idea of Rust, is implemented through ownership. It prevents memory errors at compile time and costs nothing at runtime.

Ownership works by three rules.

  1. Every value has exactly one owner.
  2. When the owner goes out of scope, the value is released (dropped automatically).
  3. Single ownership can be moved.
  • Stack and heap

    • Stack: data whose size is fixed at compile time (integers, floats, bool, and so on)
    • Heap: data allocated dynamically at runtime (String, Vec<T>, and so on)
  • Move

    • Assigning a heap-backed variable to another variable moves ownership; the original variable is no longer valid and cannot be used.

    • Example:

      rust
      let s1 = String::from("hello");
      let s2 = s1; // ownership of s1 moves to s2
      // s1 is no longer valid
  • Copy

    • Simple stack-allocated types such as i32 and bool implement the Copy trait, so assignment copies the bits (deep copies are the job of Clone).

    • Example:

      rust
      let x = 5;
      let y = x; // both x and y remain usable (copied)

References and borrowing

To use a value without taking ownership, borrow it with a reference (&).

  • Reference
    • The & operator borrows the value held by another variable.

    • Ownership does not move; only read access is borrowed.

    • Example:

      rust
      fn main() {
          let s1 = String::from("hello");
          let len = calculate_length(&s1); // pass s1 by reference
          println!("length: {}, value: {}", len, s1);
      }
       
      fn calculate_length(s: &String) -> usize {
          s.len()
      }
    • Passing &s1 to a function leaves ownership of s1 unchanged.

Mutable references

Modifying a borrowed value requires a mutable reference (&mut), and only one may exist at a time.

  • The &mut keyword creates a mutable reference.

  • Within a given scope, only one mutable reference may exist, which is what prevents data races.

  • Example:

    rust
    let mut s = String::from("hello");
    change(&mut s);
     
    fn change(some_string: &mut String) {
        some_string.push_str(", world");
    }

Smart pointer basics: Box<T>

When data belongs on the heap and only a pointer should remain on the stack, Box<T> is the simplest smart pointer for the job.

  • Box<T>
    • Stores data on the heap and keeps only the pointer (address) on the stack.

    • It is also commonly used for trait objects.

    • A short example:

      rust
      let b = Box::new(5);
      println!("b = {}", b);
    • A Box drops the data it points to automatically when it goes out of scope.

Control flow

Branching and looping resemble other C-family languages, except that match is considerably more capable.

  • if / else if / else

    rust
    let number = 7;
    if number < 5 {
        println!("small");
    } else if number > 10 {
        println!("large");
    } else {
        println!("in between");
    }
  • match

    • Branching by pattern matching, similar to switch but stronger.
    rust
    let x = 3;
    match x {
        1 => println!("1!"),
        2 | 3 => println!("2 or 3!"),
        _ => println!("something else"),
    }
  • while

    • Repeats while the condition is true.
    rust
    while condition {
        // ...
    }
  • loop

    • An infinite loop, exited with break or return.
    rust
    loop {
        println!("looping");
        break;
    }
  • for

    • Mainly used to iterate over collections.
    rust
    let arr = [10, 20, 30];
    for element in arr.iter() {
        println!("value: {}", element);
    }

Collections

The standard library ships the basic collections: growable lists, strings, and key-value maps.

  • Vector<T> (Vec<T>)

    • A variable-length list. Unlike an array, it can grow or shrink at runtime.

    • Example:

      rust
      let mut v = Vec::new();
      v.push(1);
      v.push(2);
  • String

    • A mutable, heap-allocated string.
    • push_str and push append to it.
  • HashMap<K, V>

    • A structure that stores key-value pairs.

    • Provided by std::collections.

    • Example:

      rust
      use std::collections::HashMap;
       
      let mut scores = HashMap::new();
      scores.insert("Blue", 10);
      scores.insert("Red", 50);

Enums and pattern matching

An enum groups several related kinds of value into one type, and match branches on them.

  • Enums

    • Express several related kinds of value as a single type.

    • Example:

      rust
      enum IpAddrKind {
          V4,
          V6,
      }
       
      fn route(ip_kind: IpAddrKind) { /* ... */ }
       
      fn main() {
          let four = IpAddrKind::V4;
          let six = IpAddrKind::V6;
          route(four);
          route(six);
      }
  • Branching with match

    • Each variant can trigger different behavior.
    rust
    enum Coin {
        Penny,
        Nickel,
        Dime,
        Quarter,
    }
     
    fn value_in_cents(coin: Coin) -> u8 {
        match coin {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }

Option and Result

Instead of null, Rust encodes presence and absence in Option; instead of exceptions, it encodes errors in Result.

  • Option<T>

    • Replaces the concept of null by expressing presence or absence in the type system.
    • It has two variants, Some(T) and None.
    rust
    let some_number = Some(5);
    let absent_number: Option<i32> = None;
  • Result<T, E>

    • An enum for handling errors.
    • It has the variants Ok(T) and Err(E).
    • The ? operator propagates errors concisely.
    rust
    fn read_file() -> Result<String, io::Error> {
        let mut s = String::new();
        File::open("hello.txt")?.read_to_string(&mut s)?;
        Ok(s)
    }
    • Explicit error handling keeps runtime failures under control and makes program flow predictable.

The lifetime concept

A lifetime is the compiler's mechanism for tracking the range over which a reference stays valid.

  • In Rust, a lifetime means the scope for which a reference is valid.
  • The compiler checks whether references with different lifetimes are used safely. Where inference is possible it derives lifetimes automatically; where it is not, it requires explicit annotations.
  • Goal: prevent dangling pointers and double frees at compile time.

Lifetimes on function parameters

When a function takes references and the compiler cannot infer how long they live, a lifetime parameter such as 'a is written explicitly.

rust
// Lifetime parameter 'a marks that the input references x and y and the return value all live within the same lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
 
fn main() {
    let s1 = String::from("long string");
    let s2 = "short";
 
    let result = longest(s1.as_str(), s2);
    println!("longer string: {}", result);
}

'a is the conventional name for a lifetime parameter; when several are needed, 'b, 'c, and so on follow.

Structs and lifetimes

When a struct holds a reference field, the validity range of that reference must be expressed as a lifetime parameter.

rust
struct ImportantExcerpt<'a> {
    part: &'a str,
}
 
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
 
fn main() {
    let novel = String::from("Rust is fun. Really fun!");
    let first_sentence = novel.split('.').next().expect("no sentence found");
    let i = ImportantExcerpt { part: first_sentence };
    println!("Excerpt: {}", i.part);
}

Methods on such a struct either reuse the lifetime parameter declared on the struct or, in some cases, need a lifetime of their own.

Lifetime elision

Elision rules let the compiler handle the common cases without annotations and demand explicit ones only where the relationship is genuinely ambiguous.

  • Through lifetime elision rules, the compiler builds without error whenever the lifetimes are inferable. The typical cases it infers automatically are the following.
    1. When there is exactly one input reference -> the return value gets the same lifetime automatically
    2. When the first parameter of a method is &self (or &mut self), among others
  • When there are several references or the scopes interlock in complex ways, inference fails and the compiler reports an error, so the relationship must be written out with 'a and friends.

Generics

A type parameter (<T>) generalizes a function or struct so that it works across many types.

rust
// generic function example
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list {
        if item > max {
            max = item;
        }
    }
    max
}
fn main() {
    let numbers = vec![1, 2, 3, 10, 4];
    println!("{}", largest(&numbers)); // 10
}

Type parameters such as <T> and <U> accept different types, and several may be declared at once. impl<T> makes structs and methods generic as well.

Defining and implementing traits

A trait defines the set of methods a type must provide. It is close to an interface in C++ or Java, with some differences.

rust
pub trait Summary {
    fn summarize(&self) -> String;
    
    // default method
    fn summarize_author(&self) -> String {
        String::from("(no author information)")
    }
}

Implementing the trait (impl) defines the concrete methods for each type.

rust
struct NewsArticle {
    headline: String,
    author: String,
}
 
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} by {}", self.headline, self.author)
    }
    // summarize_author has a default implementation, so overriding is optional
}

Trait bounds

A trait bound restricts a generic function or struct to types that implement a particular trait.

rust
// the parameter type T must implement the Summary trait
fn notify<T: Summary>(item: &T) {
    println!("Breaking news: {}", item.summarize());
}

With several bounds, joining them with + or moving them into a where clause keeps the signature readable.

rust
fn some_function<T, U>(t: T, u: U) 
    where T: Display + Clone,
          U: Clone + Debug
{
    // ...
}

Trait objects

A trait object defers the decision about which type implements a trait from compile time to runtime, which is dynamic dispatch. The common form is Box<dyn Trait>.

rust
fn main() {
    let article = NewsArticle { /* ... */ };
    let tweet = Tweet { /* ... */ };
 
    // different types, but both implement the Summary trait
    let items: Vec<Box<dyn Summary>> = vec![
        Box::new(article),
        Box::new(tweet),
    ];
 
    for item in items {
        println!("summary: {}", item.summarize());
    }
}

Since the type is not fixed at compile time, this incurs runtime overhead through a virtual method table.

Using Result and propagating errors

Rust handles errors by returning Result<T, E> from functions rather than by throwing exceptions.

  • A function returns Err on failure and Ok on success, leaving the caller to decide what to do.

  • The ? operator propagates errors concisely from inside a function.

    rust
    fn read_file(path: &str) -> Result<String, std::io::Error> {
        let mut s = String::new();
        std::fs::File::open(path)?.read_to_string(&mut s)?;
        Ok(s)
    }
  • Careful error handling keeps runtime failures under control and prevents unpredictable termination.

thiserror and anyhow

As a project grows, error handling needs structure, and two libraries dominate that job.

  • thiserror: a macro-based library that makes custom error types easy to define

  • anyhow: wraps errors of many kinds into a single type (anyhow::Error) for simple propagation and easy cause tracking.

    rust
    use thiserror::Error;
     
    #[derive(Error, Debug)]
    pub enum MyError {
        #[error("cannot open file: {0}")]
        FileOpenError(std::io::Error),
        #[error("invalid input data")]
        InvalidInput,
    }
     
    // anyhow usage example
    use anyhow::{Context, Result};
     
    fn do_something() -> Result<()> {
        let content = std::fs::read_to_string("config.toml")
            .with_context(|| "error while reading config.toml")?;
        // ...
        Ok(())
    }

Panics and recoverable versus unrecoverable errors

Errors split into recoverable and unrecoverable; the latter are handled with panic!.

  • panic!: called in fatal situations where the program cannot continue correctly.
  • Unrecoverable errors: cases where continuing makes no logical sense, such as an out-of-bounds access or a fatal bug.
  • Recoverable errors: I/O errors, network errors, and anything else that Result can express.
  • unwrap and expect: panic immediately on error and stop execution. They are convenient in demos and quick tests, but production code needs real error handling.

Key standard library modules

The standard library groups frequently used functionality into modules for collections, files, and input/output.

  • std::collections:
    • Vec<T>, HashMap<K, V>, HashSet<T>, BTreeMap<K, V>, LinkedList<T>, and more
  • std::fs:
    • Opening, reading, and writing files (File, read_to_string, write, and so on)
  • std::io:
    • I/O streams (stdin, stdout), buffers (BufReader, BufWriter), errors (Error), and so on
  • Other useful modules include std::thread, std::sync (concurrency), and std::time (timing).

Cargo: dependencies and builds

Cargo handles dependency management, release and debug builds, and build scripts in one tool.

  • Dependency management: add a crate under the [dependencies] section of Cargo.toml, and cargo build downloads and builds it automatically.

  • Release and debug builds:

    • Debug: the default mode, fast to build, minimally optimized

      bash
      cargo build
    • Release: maximally optimized, slower to build

      bash
      cargo build --release
  • Build scripts (build.rs): used when code must run at build time, such as generating protocol buffers or building a C library. Cargo runs build.rs before the build to perform that work.

Community and ecosystem

Several resources cover library search, examples, and lists of popular projects.

  • crates.io:
    • The official Rust package registry, where libraries are easy to search and install.
    • cargo add crate_name (supported from Cargo 1.62) adds a dependency conveniently.
  • Rust Cookbook:
    • A collection of examples from the official Rust documentation, covering common tasks such as file I/O, string parsing, and HTTP requests.
  • Awesome Rust:
    • A GitHub-maintained list of popular libraries, examples, and projects, useful for surveying the ecosystem or finding a library.

The module system and visibility

The module system splits source code along logical lines, and pub decides what each part exposes.

  • Definition: declare with mod my_module, or use a separate file my_module.rs

  • Importing: use crate::my_module::SubModule;

  • Visibility control: the pub keyword decides whether an item is reachable from outside the module

    rust
    // src/lib.rs
    pub mod network {
        pub fn connect() {
            println!("attempting network connection");
        }
        
        fn private_helper() {
            println!("internal network helper");
        }
    }

Packages and crate structure

A package consists of one or more crates, and crates come in library and binary forms.

  • Package: a unit managed by Cargo, made up of one or more crates
    • The root directory containing Cargo.toml is the starting point of the package
  • Crate:
    • Library crate (lib.rs): a library that other programs can pull in
    • Binary crate (main.rs): an executable binary, the program entry point
  • A single package may contain both src/main.rs (binary) and src/lib.rs (library).

Workspaces

Large projects group several packages under one root with a workspace.

  • A workspace lets each package keep its own Cargo.toml while sharing a common Cargo.lock and build output, which makes management more efficient.

    toml
    # root Cargo.toml
    [workspace]
    members = [
        "core-lib",
        "cli-tool",
    ]
  • The core-lib and cli-tool directories each become a package, and grouping them into a workspace lets builds and dependencies be managed together.

Associated types

An associated type lets the type implementing a trait specify a type the trait uses internally.

The canonical example is type Item on the Iterator trait.

rust
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Compared with a generic parameter (<T>), an associated type makes it easier to tie related type information together across the methods of a trait.

Default generic parameters and advanced bounds

Rust supports default types for generic parameters as well as requirements that apply only under specific conditions.

rust
// example of a default type on a generic parameter
trait MyTrait<T = i32> {
    fn do_something(&self, x: T);
}

Where clauses and advanced trait bounds also express multiple type constraints cleanly.

rust
fn complex_function<T, U>(arg: T, data: U)
    where T: MyTrait + AnotherTrait,
          U: Debug + Clone,
{
    // ...
}

This syntax keeps complex type constraints explicit and maintainable.

Abstraction patterns

Rust supports both object-oriented and functional styles, so polymorphism can be implemented in several ways.

  • Rust supports traditional object-oriented patterns (trait objects, Box<dyn Trait>) alongside functional-style abstraction through higher-order functions (iterators, closures).
  • Advanced trait design allows polymorphism in several forms.
    • Static dispatch versus dynamic dispatch
    • Generics plus trait bounds versus trait objects

Subtyping and variance

Lifetimes have a subtyping relationship, and different kinds of reference vary differently.

  • In Rust's lifetime system, a lifetime 'a can be treated as a subtype of 'b in certain cases. For example, the 'static lifetime outlives every other lifetime.
  • Invariance, covariance, and contravariance are the concepts needed to understand why the compiler rejects certain lifetimes in complex reference structures.
    • &'a mut T is mostly invariant
    • &'a T is mostly covariant

The 'static lifetime and pointers

'static denotes a reference valid for the entire duration of the program, and misusing it leads to undefined behavior.

  • The 'static lifetime means a reference valid from program start to program end. String literals ("hello") have the 'static lifetime.
  • Pointer handling: when &'static str, Box<T>, and similar are bound to the 'static lifetime, the compiler treats the memory as never freed, or as globally present.
    • Caution: misusing a 'static reference can leave a 'static reference pointing at memory that actually gets freed, which is undefined behavior, so it demands care inside unsafe Rust.

RAII and the Drop trait

Rust releases resources automatically when an object goes out of scope, and the Drop trait customizes that cleanup.

  • RAII (Resource Acquisition Is Initialization): acquiring a resource when the object is created and releasing it automatically when the object leaves scope. It matches the C++ notion of RAII.

  • In Rust, implementing the Drop trait runs custom cleanup logic when an object goes out of scope, or when ownership moves and the value is released.

    rust
    struct Resource;
     
    impl Drop for Resource {
        fn drop(&mut self) {
            println!("running resource cleanup!");
        }
    }

Threads

The standard library module std::thread spawns threads, and join waits for them to finish.

rust
use std::thread;
use std::time::Duration;
 
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("spawned thread: {}", i);
            thread::sleep(Duration::from_millis(500));
        }
    });
 
    for i in 1..5 {
        println!("main thread: {}", i);
        thread::sleep(Duration::from_millis(500));
    }
 
    // wait until the spawned thread finishes
    handle.join().unwrap();
}

Channels

A channel passes messages in a multiple-producer, single-consumer (MPSC) shape: many senders, one receiver.

rust
use std::sync::mpsc;
use std::thread;
 
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!("received: {}", received);
}

Channels make communication between threads safe.

Synchronization: Mutex, RwLock, Arc

Sharing data across threads combines locks with reference counting.

  • Mutex (mutual exclusion)

    • A synchronization primitive that protects data which must not be accessed by several threads at once.
    • The usual form is Arc<Mutex<T>>, which combines reference counting (Arc) with locking (Mutex) to share and protect data.
    rust
    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_clone = Arc::clone(&counter);
            let handle = thread::spawn(move || {
                let mut num = counter_clone.lock().unwrap();
                *num += 1;
            });
            handles.push(handle);
        }
     
        for handle in handles {
            handle.join().unwrap();
        }
     
        println!("result: {}", *counter.lock().unwrap());
    }
  • RwLock: a lock that allows many threads to read at once but only one thread to write

  • Arc (atomic reference counting): performs reference counting safely in a multithreaded environment

Asynchronous programming

Rust supports asynchronous programming with async/await syntax.

  • The async fn and await keywords return a value implementing the Future trait and express asynchronous work concisely.

    rust
    async fn do_work() {
        println!("doing async work...");
    }
     
    #[tokio::main]
    async fn main() {
        do_work().await;
    }
  • Runtime libraries such as Tokio and async-std provide reactor-based concurrency.

  • For concurrent I/O such as HTTP servers and networking, this delivers high performance and safety together.

The unsafe keyword

Inside an unsafe block, a limited set of the compiler's safety rules can be bypassed.

  • The defining feature of Rust is that the compiler enforces memory safety, but an unsafe block relaxes those rules in specific ways.
  • Operations permitted inside an unsafe block:
    1. Manipulating raw pointers (*const T, *mut T)
    2. Calling unsafe functions or methods, including FFI
    3. Accessing mutable static variables and initializing statics
    4. Bypassing compiler checks when implementing trait methods
  • Use it only where genuinely required, keep it minimal, and take care to rule out undefined behavior (UB) in advance.

Raw pointers

A raw pointer is a low-level pointer to which borrow checking and ownership checking do not apply.

  • Raw pointers are exempt from the Rust compiler's borrow and ownership checks.
  • They are used for interoperating with C and C++ code through FFI, or when low-level memory access is specifically required.

FFI: the foreign function interface

Calling a library written in C, or calling Rust from C, goes through FFI.

rust
#[link(name = "mylib")]
extern "C" {
    fn c_function(x: i32) -> i32;
}
 
fn main() {
    unsafe {
        let result = c_function(10);
        println!("C function call result: {}", result);
    }
}

Function signatures must match the ABI (application binary interface), and calls happen inside an unsafe block.

Undefined behavior caveats

The compiler makes no safety guarantees inside an unsafe block, so verification is mandatory.

  • The Rust compiler does not guarantee safety inside unsafe blocks.
  • Misusing a raw pointer, or ignoring lifetimes and touching freed memory, produces UB.
  • Writing unsafe code therefore demands rigorous verification, review, and testing.

Zero-cost abstractions

Rust's high-level syntax compiles down to code with almost no runtime overhead.

  • Rust offers high-level constructs such as generics, traits, and async while generating code with near-zero runtime cost.
  • Abstractions are optimized by the compiler and end up performing at C/C++ levels with no runtime penalty.

Inlining and SIMD

The LLVM-based compiler applies optimizations comparable to C and C++, and SIMD exploits vector instructions.

  • The Rust compiler (rustc) is built on LLVM, so it applies the same families of optimizations as C and C++: inlining, loop unrolling, auto-vectorization, and so on.
  • SIMD (single instruction, multiple data) performs parallel computation through CPU vector instructions.
    • SIMD is available through the Rust standard library and through separate crates.

Profiling

Writing high-performance code depends on finding the bottleneck.

  • perf on Linux, Instruments on macOS, and the Visual Studio profiler on Windows all work, as does cargo profiler from the Rust ecosystem, for measuring and optimizing performance.

Arena allocation and custom allocators

In special cases, a custom allocation strategy reduces the cost of allocating and freeing memory.

  • For ordinary work, the default Rust allocator is efficient enough on its own.
  • In specific settings such as game engines and real-time systems, an arena allocator or a custom allocator reduces allocation and deallocation cost.
    • Example: the bumpalo crate (a bump allocator)
    • Since Rust 1.28, the global_allocator attribute sets a custom allocator globally

Unit tests and integration tests

Rust writes unit tests with the #[test] attribute and integration tests in the tests directory.

  • Unit tests

    • Usually written to verify that a single function or module behaves as intended.
    • Functions marked with the #[test] attribute are recognized as tests when cargo test runs.
    rust
    #[cfg(test)]
    mod tests {
        use super::*;
     
        #[test]
        fn it_works() {
            assert_eq!(2 + 2, 4);
        }
    }
  • Integration tests

    • Place .rs files in the tests directory and verify whole flows through the real library or binary API.
    • They run automatically with cargo test from the project root.
  • Running tests

    • cargo test: runs every test
    • cargo test -- --nocapture: shows println! output produced during tests
    • cargo test test_name: runs only a specific test

Test doubles and TDD

Abstracting dependencies behind traits makes it easy to inject mocks for tests.

  • Building mock objects in Rust is usually done through trait-based abstraction.
    • For example, logic that touches a database or an HTTP client can sit behind a trait, with a test-only mock struct injected instead of the real implementation.
  • TDD (test-driven development): write the test first, then write the minimum implementation that makes it pass.
    • TDD works well in Rust for building reliable code.

CI/CD pipelines

A pipeline builds and tests automatically on every commit and deploys code that passes.

  • CI (continuous integration): build and test automatically on every commit and share the results

    • With GitHub Actions, a .github/workflows/*.yml file automates cargo build, cargo test, and more.
  • CD (continuous deployment): deploy code that passes tests to staging or production automatically

    • It integrates with Heroku, AWS, Netlify, Vercel, and others
  • Example (GitHub Actions):

    yaml
    name: Rust CI
     
    on: [push, pull_request]
     
    jobs:
      build_and_test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Install Rust
            uses: dtolnay/rust-toolchain@stable
          - name: Build
            run: cargo build --verbose
          - name: Test
            run: cargo test --verbose

Code style: rustfmt and clippy

The official formatter and linter keep style consistent and code quality high.

  • rustfmt: the official code formatter, which reformats Rust code into a consistent style automatically.
    • cargo fmt applies it in one command
    • A .rustfmt.toml file configures the details
  • clippy: the Rust linter, which reports potential errors, recommended style, and performance improvements.
    • Run it with cargo clippy for fast feedback that raises code quality

Refactoring

Refactoring reduces duplication and keeps the structure simple.

  • Extracting functions: splitting duplicated logic or overly complex functions
  • Trait abstraction: defining a shared trait across types with similar behavior to remove duplication
  • Redesigning visibility (pub, pub(crate)) and module structure to keep the project layout compact

API design and documentation

Separate the public API from internal implementation, and generate documentation from /// comments.

  • When shipping a library or binary, separating the public API from internal implementation details improves maintainability.

  • In Rust, documentation comments written with /// generate a documentation website through cargo doc.

    rust
    /// Adds two numbers together.
    ///
    /// # Examples
    ///
    /// ```
    /// assert_eq!(add(2, 3), 5);
    /// ```
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

HTTP clients and servers

The Rust ecosystem provides both HTTP clients and web frameworks.

  • reqwest:

    • A Rust library for sending HTTP requests easily, with simple calls for GET, POST, and other methods
    rust
    let body = reqwest::blocking::get("https://www.rust-lang.org")?
        .text()?;
    println!("Body = {}", body);
  • hyper:

    • A low-level HTTP library that provides high-performance asynchronous HTTP servers and clients
  • actix-web, rocket:

    • Web frameworks for building web servers in Rust quickly, with routing, middleware, sessions, and authentication built in

    • Example: a minimal actix-web server

      rust
      use actix_web::{get, web, App, HttpServer, Responder};
       
      #[get("/")]
      async fn index() -> impl Responder {
          "Hello from Actix!"
      }
       
      #[actix_web::main]
      async fn main() -> std::io::Result<()> {
          HttpServer::new(|| App::new().service(index))
              .bind(("127.0.0.1", 8080))?
              .run()
              .await
      }

Serialization and deserialization: serde

serde serializes and deserializes Rust data structures across many formats.

  • The serde library:

    • Serializes and deserializes Rust data structures to and from JSON, TOML, YAML, MessagePack, and other formats.
    • It works through the #[derive(Serialize, Deserialize)] macro
    rust
    use serde::{Serialize, Deserialize};
     
    #[derive(Serialize, Deserialize)]
    struct Config {
        name: String,
        version: u32,
    }
     
    fn main() {
        let json_str = r#"{ "name": "MyApp", "version": 1 }"#;
        let cfg: Config = serde_json::from_str(json_str).unwrap();
        println!("name: {}, version: {}", cfg.name, cfg.version);
    }

Designing a REST API

A REST API is built from routing, middleware, and authentication.

  • Routing: mapping HTTP paths and methods (GET, POST, and so on) to endpoint functions
  • Middleware: a layer for shared concerns such as authentication, logging, and error handling
  • Authentication: token-based authentication using JWT (JSON Web Token), OAuth, or similar
    • Rust web frameworks either ship middleware and libraries for authentication or leave room for a custom implementation.

System interfaces

The standard library and FFI provide access to files, sockets, and OS-level APIs.

  • The Rust standard library modules std::fs and std::net give access to low-level system resources such as files and sockets
  • Interacting with OS-level APIs such as Linux syscalls or the Windows Win32 API often uses FFI (extern "C") and unsafe blocks.

Embedded Rust

Embedded work uses no_std mode and a HAL to drive microcontrollers.

  • Embedded targets may require building in no_std mode rather than against the usual standard library (std).
    • That mode provides a minimal environment that works without heap allocation or OS services
  • An embedded Rust HAL (hardware abstraction layer) implements pin control, interrupt handling, and similar work in Rust on specific microcontrollers such as the ARM Cortex-M family.

WASM: WebAssembly

Rust code compiles to WebAssembly (WASM) and runs in browsers or on the server side.

  • wasm-bindgen: a tool that bridges Rust and JavaScript
  • wasm-pack: a tool that builds Rust code and packages it for npm distribution
  • WASM is useful where real-time performance matters (games, simulations, image processing) or where a safe sandbox is required.

Application design

Tying the learned concepts into a working application is what makes them stick.

  • Combining the Rust knowledge covered so far, including ownership, traits, concurrency, and web frameworks, into a real service is the important step.
  • Examples: CLI tools, web services, embedded projects, blockchain nodes, game engines
  • During planning, pin down the project scope, the libraries involved, the data model, the API specification, and the test scenarios

Contributing to open source

The Rust ecosystem is actively open source, so contributing builds practical experience.

  • Contributing to a library or project of interest is an effective way to build practical experience.
  • Start with small documentation fixes, then move on to issue resolution and pull requests for new features.

Code review and best practices

On team projects, code review covers ownership, lifetimes, error handling, and style together.

  • Reviewing Rust code as a team surfaces and shares knowledge about ownership, lifetimes, error handling, and coding style.
  • Rust best practices
    • Handle errors as explicitly as possible
    • Prioritize safety and minimize unsafe
    • Balance performance against readability
    • Treat documentation and tests as mandatory
    • Resolve clippy warnings to keep code quality high

Summary

Rust guarantees memory safety at compile time through three mechanisms: ownership, the borrow checker, and lifetimes. Generics and traits provide abstraction without runtime cost, while async/await and Arc/Mutex support safe concurrency. unsafe and FFI stay confined to the boundaries where low-level control is genuinely required. The standard library, Cargo, and the crates.io ecosystem cover testing, serialization, web servers, embedded work, and WebAssembly. Connecting these concepts to a real project and working on a larger codebase is what makes the ownership and lifetime rules feel natural.