6.4 Rust's Borrowing Rules in Detail

Rust’s memory safety is built on the following borrowing rule: Given an object T, only one of these conditions can hold at any time:

  • Multiple immutable references (&T) to the object (referred to as aliasing).
  • A single mutable reference (&mut T) to the object (referred to as mutability).

These rules are crucial in multi-threaded programming, where they prevent problems like data races. However, in single-threaded code, these rules might seem overly restrictive, and their advantages may not be immediately apparent.

To better understand their value, we will explore the benefits of these rules in more detail. It is also worth noting that Rust provides the concept of internal mutability, which allows controlled relaxation of these strict rules when necessary.

6.4.1 Benefits of Rust's Borrowing Rules for References

Rust's memory safety rules, particularly those governing mutable and immutable references, enforce strict guarantees about how data is accessed in memory. These rules offer several benefits:

  1. Prevent Data Races:

    • In a multithreaded context, a data race occurs when two or more threads access the same memory location simultaneously, with at least one modifying it, and there is no synchronization mechanism.
    • Rust’s rules prevent such scenarios by ensuring that mutable access is exclusive. Even in single-threaded applications, this eliminates the possibility of accidental interference from concurrent-like behavior, such as callbacks or re-entrant code.
  2. Guarantee Consistency:

    • Immutable references ensure that the data they point to cannot change, guaranteeing that all readers see a consistent view of the data. This makes reasoning about code much easier and reduces potential bugs caused by unexpected modifications.
  3. Avoid Undefined Behavior:

    • In languages like C or C++, undefined behavior can arise when mutable data is aliased (i.e., multiple pointers to the same object exist, and one modifies it). Rust's rules prevent such situations, eliminating a significant class of bugs.
  4. Enable Compiler Optimizations:

    • Because the compiler knows that data cannot be modified through immutable references or that no aliasing exists with mutable references, it can safely optimize memory access patterns. For example, it might cache values or reorder instructions more aggressively.
  5. Simplify Ownership and Lifetimes:

    • These rules align with Rust's ownership system, making it easier to understand and verify the scope and validity of references at compile time. They also help ensure that dangling pointers (references to deallocated memory) cannot exist.

6.4.2 Risks Without These Rules in Single-Threaded Applications

Without these rules, even in single-threaded applications, several issues can arise:

  1. Data Corruption:

    • Multiple references to the same object can lead to inconsistent or corrupted data if one reference modifies the object while others assume it remains unchanged.
  2. Hard-to-Debug Bugs:

    • Changes made through one reference might unexpectedly affect others. For instance, if one pointer to a data structure modifies it, and another pointer tries to read or modify the same data, the program might exhibit undefined behavior, which is difficult to diagnose and reproduce.
  3. Invalid Reads:

    • If one reference deallocates or modifies an object while another attempts to access it, this can lead to crashes or invalid data being read.
  4. Loss of Program Predictability:

    • When aliasing and mutability are allowed simultaneously, the program's behavior becomes harder to predict because the state of an object might change unexpectedly through a different reference.
  5. Broken Invariants:

    • Many data structures rely on maintaining internal consistency (invariants). If multiple references can modify an object concurrently, these invariants may be violated, causing the program to behave incorrectly.

6.4.3 Example in C Without Rules

Consider the following example in C:

#include <stdio.h>

void modify(int *a, int *b) {
    *a = 42;  // Modify value through one pointer
    *b = 99;  // Modify value through another pointer
}

int main() {
    int x = 10;
    modify(&x, &x); // Pass the same reference twice
    printf("x = %d\n", x); // What will this print?
    return 0;
}

In this case:

  • The behavior depends on the order of modifications (*a = 42 and *b = 99).
  • The compiler might reorder instructions or optimize accesses, leading to unpredictable results.
  • In Rust, this would result in a compile-time error because you cannot have both &x (immutable) and &mut x (mutable) simultaneously.

6.4.4 Rust's Approach

Rust eliminates such ambiguities by enforcing the reference rule at compile time. This prevents accidental or undefined behavior and ensures that all memory access is predictable, safe, and well-defined. This strict enforcement is a key reason why Rust is trusted for writing safe and efficient systems programming code.