Skip to content

Advanced Rust Programming: Ownership, Borrowing, and Encapsulation

Introduction

Rust is a programming language that has evolved significantly since its inception. Originally defined in 2015 as "a systems programming language that runs blazingly fast, prevents nearly all segfaults and guarantees thread safety," it was redefined in 2018 as "a language empowering everyone to build reliable and efficient software."

This guide explores advanced Rust concepts focusing on reliability through ownership, borrowing, and encapsulation—going beyond just memory safety to examine how Rust manages complex program interactions and prevents "action at distance" bugs where changes in one component unexpectedly break another.

Core Language Philosophy

Rust fundamentally deals with two main constructs:

  1. Data - Structures (multiple fields) and enums (alternatives)
  2. Functions - Everything is a function call, including data conversion and component interaction

Unlike languages with classes and inheritance, Rust has no automatic virtual dispatch and follows a more functional approach to program design.

Ownership: The Foundation

Basic Ownership Rules

Every value in Rust has exactly one unique owner. This owner can: - Mutate the data - Destroy the data
- Pass ownership to other parts of the system

When ownership is lost or given up, the value is automatically dropped and cleaned up.

struct Point {
    x: i32,
    y: i32,
}

impl Drop for Point {
    fn drop(&mut self) {
        println!("Dropping point");
    }
}

fn main() {
    let point = Point { x: 1, y: 2 };
    println!("{:?}", point);
    // Point is automatically dropped here when it goes out of scope
}

What Can You Own?

Ownership in Rust extends far beyond simple data:

  • Plain data: Numbers, basic structures
  • Heap allocations: Vectors, dynamically allocated memory
  • Resources with lifecycles: File handles, network connections
  • Abstract privileges: Mutex guards, exclusive access tokens
use std::fs::File;

// Owning a simple data structure
let point = Point { x: 1, y: 2 };

// Owning a file resource
let file = File::open("example.txt")?;
// When `file` goes out of scope, it's automatically closed

Resource Management, Not Just Memory

Ownership is fundamentally about resource safety. A File in Rust doesn't expose its internal file descriptor because:

  1. It maintains encapsulation of the resource lifecycle
  2. It prevents interference with proper cleanup
  3. It ensures resources are properly released

Notably, Rust's File has no close() method—the way to close it is to drop ownership.

Borrowing: Controlled Access

Borrowing allows values to be referenced while the owner promises not to modify or destroy the data. There are two types of references:

Immutable References (&T)

fn takes_borrow(point: &Point) {
    println!("Point: ({}, {})", point.x, point.y);
}
  • Can be shared (multiple immutable references allowed)
  • Guarantee: No mutation will be observed through these references
  • Stronger than just "read-only"—nobody can mutate the data while borrowed

Mutable References (&mut T)

fn modify_point(point: &mut Point) {
    point.x += 1;
    point.y += 1;
}
  • Must be unique (only one mutable reference at a time)
  • Cannot alias with other references
  • Provides exclusive access for the duration of the borrow

Borrow Checking

Rust uses region-based memory management to verify borrows:

fn main() {
    let data = Point { x: 1, y: 2 };     // data lives: line 1-4
    let reference = &data;                // borrow lives: line 2-4
    drop(data);                          // ❌ Error: trying to drop at line 3
    println!("{:?}", reference);         // reference used until line 4
}

The compiler draws regions and ensures borrowed data outlives all references to it.

Ownership vs Borrowing in Software Architecture

The choice between ownership and borrowing has significant architectural implications:

Ownership as Decoupling

fn submit_write_operation(file: File, data: String) {
    // Takes ownership - caller gives up control
    // Enables fire-and-forget patterns
    // Good for component boundaries
}

Borrowing as Coupling

fn write_to_disk(file: &mut File, data: &str) -> std::io::Result<()> {
    // Borrows - creates tight coupling
    // Caller must maintain data and file
    // Good for controlled, short-term operations
}

Key insight: Fighting with the borrow checker often indicates architectural issues rather than language limitations.

Interior Mutability and Encapsulation

Rust provides mechanisms to safely violate the borrowing rules when needed, through interior mutability patterns.

Mutex: A Practical Example

A mutex demonstrates how Rust models runtime-enforced unique access:

use std::sync::Mutex;

struct Counter {
    value: i32,
}

fn main() {
    let data = Counter { value: 0 };
    let mutex = Mutex::new(data);  // Mutex takes ownership

    manipulate_counter(&mutex);    // Pass immutable reference to mutex
}

fn manipulate_counter(counter: &Mutex<Counter>) {
    let mut guard = counter.lock().unwrap();
    guard.value += 1;  // Mutable access through immutable reference!
}

How Interior Mutability Works

The mutex provides:

  1. Encapsulation: The inner data is hidden behind the mutex interface
  2. Runtime enforcement: Locking ensures unique access at runtime
  3. Safe interface: Interior mutability is contained within the mutex implementation
// Conceptual implementation
pub struct Mutex<T> {
    data: UnsafeCell<T>,  // Allows interior mutability
    // ... locking machinery
}

pub struct MutexGuard<'a, T> {
    mutex: &'a Mutex<T>,  // Tied to original mutex lifetime
}

impl<T> Mutex<T> {
    pub fn lock(&self) -> MutexGuard<T> {
        // Acquire lock from OS
        MutexGuard { mutex: self }
    }
}

impl<T> MutexGuard<'_, T> {
    pub fn borrow_mut(&mut self) -> &mut T {
        unsafe {
            // Safety: We hold the lock, so access is unique
            &mut *self.mutex.data.get()
        }
    }
}

Unsafe Rust: Controlled Danger

The unsafe keyword doesn't disable safety checks—it enables additional capabilities that require manual verification:

  1. Dereferencing raw pointers
  2. Calling unsafe functions
  3. Accessing mutable statics
  4. Implementing unsafe traits
  5. Reading from unions
  6. Accessing fields of packed structs

Safety Through Encapsulation

impl<T> MutexGuard<'_, T> {
    fn borrow_mut(&mut self) -> &mut T {
        unsafe {
            // Safety argument:
            // 1. We own the MutexGuard, proving we hold the lock
            // 2. MutexGuard cannot be cloned or copied
            // 3. Lock ensures exclusive access
            // 4. Lifetime ties reference to guard
            &mut *self.mutex.data.get()
        }
    }
}

The key insight: The amount of unsafe code isn't what matters—it's the strength of the safety argument and the encapsulation around it.

Key Principles for Advanced Rust

When working with advanced Rust concepts, always ask:

  1. What resources do I manage? Beyond memory, consider files, locks, network connections
  2. What are their lifecycles? How are they acquired and released?
  3. How can I encode invariants? Use the type system to prevent invalid states
  4. What is the relationship between resources? Model dependencies through ownership
  5. How do I encapsulate complexity? Hide internal management behind safe interfaces
  6. If using unsafe, what is my safety argument? Document why the unsafe code is correct

Conclusion

Advanced Rust programming is fundamentally about:

  • Ownership for lifecycle management and component decoupling
  • Borrowing for controlled access with clear contracts
  • Encapsulation for hiding complexity and managing invariants
  • Interior mutability for safe violations of borrowing rules
  • Unsafe code for operations the compiler cannot verify

These features work together to prevent "action at distance" bugs and enable reliable software architecture. The strict nature of Rust's ownership system forces explicit thinking about resource management and component boundaries, leading to more maintainable and reliable code.