| Contents |

6 Ownership and memory management

In C, manual memory management is a central aspect of programming. Developers allocate and deallocate memory using malloc and free, which provides flexibility but is notoriously prone to errors like memory leaks, dangling pointers, and use-after-free bugs. C++ introduced RAII (Resource Acquisition Is Initialization) and smart pointers to automate resource management, reducing some risks. Many higher-level languages (Java, Python, Go, etc.) employ garbage collection (GC), which simplifies memory management significantly but often introduces runtime overhead and non-deterministic pauses, making it less suitable for performance-critical systems or embedded environments.

Rust presents a unique alternative: compile-time memory safety without a garbage collector. It achieves this through a system of ownership, borrowing, and lifetimes, enforced by the compiler. This approach ensures memory safety with minimal runtime overhead, making Rust a compelling choice for systems programming.

This chapter introduces these core concepts, primarily using Rust’s String type as an example. Its dynamic, heap-allocated nature makes it ideal for illustrating ownership principles clearly. We’ll compare Rust’s mechanisms with C/C++ idioms where helpful. We will also briefly touch upon Rust’s smart pointers and the unsafe keyword for scenarios requiring more manual control or C interoperability, deferring deep dives to later chapters (Chapters 19 and 25).


6.1 The Ownership System

In Rust, every value has a variable that is its owner. The ownership system is governed by a simple set of rules enforced at compile time by the borrow checker:

  1. Single Owner: Each value in Rust has exactly one owner at any given time.
  2. Scope-Bound Cleanup (Drop): When the owner goes out of scope, the value it owns is dropped (its resources, like memory, are automatically deallocated). This “drop scope” is lexical; destructors run at the end of the block where the variable is declared, not necessarily immediately after its last use.
  3. Ownership Transfer (Move): Assigning a value from one variable to another, or passing it by value to a function, moves ownership. The original variable becomes invalid.

This system prevents common memory errors like double frees (since only one owner can drop the value) and use-after-free (since variables become invalid after moving ownership).

If custom cleanup logic is needed when a value is dropped (e.g., releasing file handles or network sockets), you can implement the Drop trait, similar in concept to a C++ destructor.

6.1.1 Scope and Automatic Cleanup (Drop)

Consider this Rust code:

fn main() {
    {
        let s = String::from("hello"); // s comes into scope, allocates memory
        // ... use s ...
    } // s goes out of scope here. Rust calls drop on s, freeing its memory.
}

When s goes out of scope, Rust automatically calls the necessary cleanup code for String, freeing its heap-allocated buffer.

6.1.2 Comparison with C

In C, the equivalent requires manual intervention:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    {
        char *s = malloc(6); // Allocate memory
        if (s == NULL) { /* handle allocation error */ return 1; }
        strcpy(s, "hello");
        // ... use s ...
        free(s); // Manually free the memory is crucial
    } // Forgetting free(s) causes a memory leak.
    return 0;
}

Rust’s automatic dropping based on scope prevents leaks without requiring manual free calls.


6.2 Transferring Ownership: Move, Copy, and Clone

How data is handled during assignment or function calls depends on its type. Rust distinguishes between moving, copying, and cloning.

6.2.1 Move Semantics

Types that manage resources on the heap, like String, Vec<T>, or Box<T>, use move semantics by default. When ownership is transferred (either through assignment to another variable or by passing the value to a function), the underlying resource is not duplicated; only the “control” (ownership) moves. The original variable binding becomes invalid.

Move via Assignment:

fn main() {
    let s1 = String::from("allocated"); // s1 owns the string data on the heap
    let s2 = s1; // Ownership MOVES from s1 to s2. s1 is now invalid.
    // println!("s1: {}", s1); // Compile-time error! s1's value was moved.
    println!("s2: {}", s2); // s2 now owns the data. Prints: allocated
} // s2 goes out of scope, its owned string data is dropped.

Move via Function Arguments:

Passing a value to a function transfers ownership in the same way.

fn takes_ownership(some_string: String) {
    // `some_string` takes ownership of the passed value
    println!("Inside function: {}", some_string);
} // `some_string` goes out of scope, Drop is called, memory is freed.

fn main() {
    let s = String::from("hello"); // s comes into scope
    takes_ownership(s);             // s's value moves into the function...
                                    // ...and is no longer valid here.
    // println!("Moved string: {}", s); // Compile-time error! s was moved.
}

Move via Function Return Values:

Similarly, returning a value from a function moves ownership out of the function to the calling scope.

fn creates_and_gives_ownership() -> String { // Function returns a String
    let some_string = String::from("yours"); // some_string comes into scope
    some_string                             // Return some_string, moving ownership out
}

fn main() {
    let s1 = creates_and_gives_ownership();
    // Ownership moves from function return to s1
    println!("Got ownership of: {}", s1);
} // s1 is dropped here.

What Actually Happens During a Move?

When a value like String (or Vec<T>, Box<T>) is moved – either through assignment (let s2 = s1;) or by passing it by value to a function (takes_ownership(s1);) – the operation is very efficient at runtime. Remember that a String value itself (the metadata) consists of a small structure holding {a pointer to the heap data, a length, a capacity}. This structure usually resides on the stack for local variables.

During a move:

  1. Bitwise Copy of Struct: The {pointer, length, capacity} structure is copied bit-for-bit from the source (s1) to the destination (s2 or the function parameter). This is a fast operation, similar to copying a simple struct in C. No heap allocation occurs for this structure itself; the bits are copied into the stack space already designated for the new variable or parameter.
  2. No Heap Interaction: The character data stored on the heap is not copied or modified. The pointer value that is copied simply points to the same heap allocation.
  3. Ownership Transfer: The responsibility for managing and eventually deallocating the heap buffer is transferred to the new variable/parameter.
  4. Invalidation: The original variable (s1) is marked as invalid by the compiler. Its destructor (Drop) will not run when it goes out of scope, preventing a double free.

In essence, a move in Rust for types that manage heap resources avoids expensive deep copies by simply copying the small, fixed-size ‘handle’ or ‘metadata’ and transferring the unique ownership rights to the underlying resource.

A Note on Function Calls and Borrowing (e.g., println!)

You might wonder why passing a String to the println! macro doesn’t move ownership, allowing you to use the String afterwards:

fn main() {
    let message = String::from("Hello, Rust!");
    println!("First print: {}", message); // Pass owned String to println!
    println!("Second print: {}", message); // Still valid, can use message again!
}

This works because println! is a macro. Macros can be more flexible than regular functions. println! expands into code that uses formatting traits, and these traits typically operate on references. When you pass an owned String, the macro expansion effectively takes a shared reference (&String, which often further dereferences to &str for formatting) for the duration of the call. It borrows the value rather than consuming it, leaving the original message variable and its ownership intact. While some generic functions can also accept different types via traits that involve borrowing (like AsRef), the specific ability of println! to seem like it takes ownership but doesn’t is characteristic of its macro implementation. Contrast this with regular functions taking String by value, which do move ownership as shown previously.

Comparison with C++ and C

Rust’s default move semantics enforce single ownership, preventing these C/C++ issues at compile time.

6.2.2 Simple Value Copies: The Copy Trait

Types whose values can be duplicated via a simple bitwise copy implement the Copy trait. This applies to types with a fixed size known at compile time that do not require special cleanup logic (i.e., they don’t implement Drop). When assigned or passed by value (either to another variable or as a function argument), variables of Copy types are duplicated (copied), and the original variable remains valid and usable. Examples include integers, floats, booleans, characters, and tuples/arrays containing only Copy types.

fn makes_copy(some_integer: i32) { // some_integer gets a copy
    println!("Inside function: {}", some_integer);
} // some_integer (the copy) goes out of scope.

fn main() {
    let x = 5;    // i32 implements Copy
    let y = x;    // y gets a COPY of x's value. x is still valid.
    println!("x: {}, y: {}", x, y); // Both usable. Prints: x: 5, y: 5
    makes_copy(x); // x is copied into the function.
    println!("x after function call: {}", x); // x is still valid and usable here.
}

These types are Copy because copying their bits is cheap and sufficient to create a new, independent value. There’s no owned resource (like a heap pointer) requiring unique ownership or cleanup via Drop. Types implementing Drop cannot be Copy, as implicit copying would make resource management ambiguous.

6.2.3 Explicit Deep Copies: The Clone Trait

If you need a true duplicate of data managed by an owning type (like String or Vec<T>) – meaning, new heap allocation and copying the data – you must explicitly request it using the .clone() method. This requires the type to implement the Clone trait (most standard library owning types do).

fn main() {
    let s1 = String::from("duplicate me");
    let s2 = s1.clone(); // Explicitly performs a deep copy. s1 remains valid.
    println!("s1: {}, s2: {}", s1, s2); // Both are valid and own independent data.
} // s1 is dropped, then s2 is dropped. Each frees its own memory.

Because cloning can be expensive (memory allocation and data copying), Rust makes it explicit via a method call. This encourages programmers to consider whether they really need a full copy or if borrowing (using references, discussed next) would be more efficient. Note that for Copy types, clone() is usually implemented as just a simple copy.


6.3 Borrowing: Access Without Ownership Transfer

Often, you need to access data without taking ownership. Rust allows this through borrowing, using references. A reference is like a pointer that provides access to a value owned by another variable, but unlike C pointers, references come with strict compile-time safety guarantees enforced by the borrow checker.

There are two fundamental types of references, which are commonly called “immutable” and “mutable” references, but are more precisely understood as providing shared access and exclusive access, respectively:

  1. Shared References (&T): Allow read-only access to the borrowed data. Multiple shared references to the same data can coexist concurrently.
  2. Exclusive References (&mut T): Allow read-write access to the borrowed data. Only one exclusive reference to a given piece of data can exist at any time.

Using the terms “shared” and “exclusive” helps clarify the fundamental nature of Rust’s borrowing rules: &T allows data to be shared among multiple readers, while &mut T grants exclusive permission to modify the data. This distinction is crucial for Rust’s compile-time memory safety.

6.3.1 References vs. C Pointers

While similar in concept to C pointers (*T), Rust references have key differences:

Feature Rust References (&T, &mut T) C Pointers (*T)
Nullability Guaranteed non-null Can be NULL
Validity Guaranteed to point to valid memory (via lifetimes) Can be dangling (point to freed memory)
Access Rules Strict compile-time rules (one exclusive XOR multiple shared) No compile-time enforcement
Arithmetic Generally not allowed (use slice methods) Pointer arithmetic is common
Dereferencing Often automatic (e.g., method calls) Explicit (*ptr or ptr->member)

Because of these guarantees, Rust references are sometimes called “safe pointers” or “managed pointers.”

Method Calls and Automatic Referencing/Dereferencing

You might notice you can call methods like .len() directly on both an owned String and a reference &String (or &str):

fn main() {
    let owned_string = String::from("hello");
    let string_ref = &owned_string;
    // Both calls work:
    println!("Owned length: {}", owned_string.len());
    println!("Ref length:   {}", string_ref.len());
}

This convenience is enabled by Rust’s method call syntax and automatic referencing and dereferencing. When you use the dot operator (object.method()), the compiler automatically adds necessary &, &mut, or * operations to make the method call match the method’s signature regarding self, &self, or &mut self.

This mechanism significantly cleans up code, avoiding manual (&value).method() or (*reference).method() calls in most situations. The Deref trait (covered later) plays a key role in this process for types like String and smart pointers.

6.3.2 The Borrowing Rules

The borrow checker enforces these core rules at compile time:

  1. Scope and Validity: A reference cannot outlive the data it refers to. References are always guaranteed to point to valid data of the expected type (no dangling or null references). (This is primarily enforced by lifetimes, detailed in Section 6.6).
  2. Access Exclusivity: At any given time, you can have either one exclusive reference (&mut T) or any number of shared references (&T) to the same piece of data. You cannot have both types of references active to the same data simultaneously.

Rule 2 ensures that you cannot obtain an exclusive reference while any shared references exist to the same data, nor can you obtain (or keep active) multiple exclusive references simultaneously. This “one or many” rule is fundamental to preventing data races and ensuring safe mutation.

Example: Shared References (Aliasing Allowed)

You can have multiple shared (immutable) references to the same data concurrently. Crucially, this is allowed whether the owner variable itself was declared with mut or not. The mut status of the owner primarily determines if exclusive borrows (&mut T) can be taken or if the owner can be directly modified, not whether shared borrows (&T) are permitted.

fn main() {
    let s1 = String::from("hello"); // Owner is not mutable
    let r1 = &s1; // Shared borrow from immutable owner
    let r2 = &s1; // Another shared borrow
    println!("r1: {}, r2: {}", r1, r2); // OK

    let mut s2 = String::from("hello"); // Owner is mutable
    let r3 = &s2; // Shared borrow from mutable owner is fine
    let r4 = &s2; // Multiple shared borrows are fine
    println!("r3: {}, r4: {}", r3, r4); // Also OK
}

This is safe because shared references guarantee the underlying data won’t change unexpectedly while they are active.

Non-Lexical Lifetimes (NLL) Example

The following example demonstrates how the compiler precisely tracks borrow durations:

    fn main() {
        let mut s1 = String::from("hello");
        let r1 = &s1;                      // (1) Shared borrow starts
        println!("r1: {}, s1: {}", r1, s1); // (2) Last use of r1 (in the success case)
        s1.push('!');                      // (3) Needs exclusive borrow of s1
        println!("s1: {}", s1);
        // println!("r1: {}", r1); // (4) Potential later use of r1
                                // -> uncommenting causes compile error
    }

This code highlights how precisely Rust’s borrow checker analyzes borrow durations, thanks to a feature called Non-Lexical Lifetimes (NLL). Introduced formally in the Rust 2018 Edition, NLL means that borrows are typically considered active only until their last actual point of use within a scope, rather than necessarily lasting for the entire lexical scope (code block) they are declared in.

Let’s trace this example:

  1. A shared borrow r1 begins.
  2. r1 is used in the println!.
  3. s1.push('!') attempts to take an exclusive borrow of s1. This is only allowed if no shared borrows (like r1) are currently active.
  4. The commented-out line represents a potential later use of r1.

This NLL behavior allows more code to compile than older versions of the borrow checker while still strictly preventing errors caused by conflicting borrows.

Example: Exclusive Reference (Exclusive Access)

You can only have one exclusive (mutable) reference to a piece of data in a particular scope. Furthermore, the variable bound to the data must be declared mut to allow exclusive borrowing.

fn main() {
    let mut s = String::from("hello"); // Must be `mut` to borrow exclusively
    let r1 = &mut s; // One exclusive borrow

    // The following lines would cause compile-time errors if uncommented:
    // let r2 = &mut s; // Error: Cannot have a second exclusive borrow.
    // let r3 = &s; // Error: Cannot have a shared borrow while an exclusive one exists.
    // s.push_str("!"); // Error: Cannot access owner directly while exclusively borrowed.

    r1.push_str(" world"); // Modify data through the exclusive reference
    println!("r1: {}", r1);
} // r1 goes out of scope here. The exclusive borrow ends.

6.3.3 Why These Rules Benefit Single-Threaded Code

The borrowing rules, especially the “one exclusive (&mut) XOR many shared (&)” rule (Access Exclusivity), might seem overly strict if you’re only thinking about multi-threaded data races. However, they are fundamental to Rust’s safety and predictability guarantees even in single-threaded code.

Consider the following example, which Rust refuses to compile:

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0]; // shared borrow occurs here
    v.push(4); // exclusive borrow occurs here
    println!("{:?} {}", v, first); // shared borrow later used here
}

This code attempts to keep a shared reference to an element of a vector while later modifying the vector. Rust rejects this pattern because changes to the vector, such as inserting a new element, may require reallocating its internal memory buffer. Such reallocation would move the elements in memory and make existing references invalid, potentially leading to undefined behavior (e.g., using a dangling pointer).

Without Rust’s strict aliasing rules, several subtle but serious problems could arise:

In summary, the borrowing rules eliminate many potential pitfalls familiar from C/C++, ensuring data consistency and predictable behavior even without considering threads. They also enable the compiler to perform more aggressive optimizations safely.

Invalid Reference Example (Dangling Pointer Prevention)

Rust also prevents references from outliving the data they point to:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // Tries to return a shared reference to a String
    let s = String::from("hello"); // s is created inside dangle
    &s // Return a reference to s
} // s goes out of scope and is dropped here. Its memory is freed.
  // The returned reference would point to invalid memory!

The compiler rejects this code because the reference &s would outlive the owner s. This is handled by Rust’s lifetime system, ensuring references are always valid.


6.4 The String Type and Memory Details

Understanding how String works internally helps clarify ownership and borrowing.


6.5 Slices: Borrowing Contiguous Data

Beyond references to entire values, Rust provides slices, which are references to a contiguous sequence of elements within a collection, rather than the whole collection. Slices provide a non-owning view (a borrow) into data owned by something else (like a String, Vec<T>, array, or even another slice). They are crucial for writing efficient code that accesses portions of data without needing to copy it or take ownership.

Internally, a slice is typically a fat pointer, storing two pieces of information:

  1. A pointer to the start of the sequence segment.
  2. The length of the sequence segment.

Because slices borrow data, they strictly adhere to Rust’s borrowing rules: you can have multiple shared slices of the same data, or exactly one exclusive slice, but not both at the same time if they could overlap.

6.5.1 Shared and Exclusive Slices

There are two primary kinds of slices, mirroring the two kinds of references:

The type T represents the element type (e.g., i32, u8).

6.5.2 Array Slices

Slices are commonly used with arrays (fixed-size lists on the stack) and vectors (growable lists on the heap).

fn main() {
    let numbers: [i32; 5] = [10, 20, 30, 40, 50]; // An array

    // Create shared slices using range syntax
    let all: &[i32] = &numbers[..];          // Slice of the whole array
    let first_two: &[i32] = &numbers[0..2];  // Slice of elements 0 and 1 ([10, 20])
    let last_three: &[i32] = &numbers[2..];  // Slice of elements 2, 3, 4 ([30, 40, 50])

    println!("All: {:?}", all);
    println!("First two: {:?}", first_two);
    println!("Last three: {:?}", last_three);

    // Create an exclusive slice (requires the owner to be mutable)
    let mut mutable_numbers = [1, 2, 3];
    let exclusive_slice: &mut [i32] = &mut mutable_numbers[1..]; // Slice of elements
    // 1 and 2. Index access refers to the slice itself: index 0 of the slice is
    // index 1 of the array.
    exclusive_slice[0] = 99;
    // mutable_numbers is now [1, 99, 3]
    println!("Modified numbers: {:?}", mutable_numbers);
}

Note: The .. range syntax creates slices: .. is the whole range, start..end includes start but excludes end, start.. goes from start to the end, and ..end goes from the beginning up to (excluding) end. This syntax works on arrays, vectors, and existing slices.

6.5.3 String Slices (&str)

A string slice, written &str, is a specific type of shared slice that always refers to a sequence of valid UTF-8 encoded bytes. It’s the most primitive string type in Rust. You can create string slices by borrowing from Strings, other string slices, or string literals using range syntax with byte indices.

fn main() {
    let s_ascii: String = String::from("hello world"); // ASCII string

    // Slicing ASCII text is straightforward as byte indices match character boundaries
    let hello: &str = &s_ascii[0..5]; // Slice referencing "hello"
    let world: &str = &s_ascii[6..11]; // Slice referencing "world"
    println!("Slice 1: {}", hello);
    println!("Slice 2: {}", world);

    // With multi-byte UTF-8 characters, indices must respect character boundaries
    let s_utf8 = String::from("你好"); // "Nǐ hǎo" - 6 bytes total, each char is 3 bytes
    // let invalid_slice = &s_utf8[0..1]; // PANIC! 1 is not a character boundary.
    // let invalid_slice = &s_utf8[0..2]; // PANIC! 2 is not a character boundary.
    let first_char: &str = &s_utf8[0..3]; // OK: Slice referencing first character "你"
    let second_char: &str = &s_utf8[3..6];//OK: Slice referencing second character "好"

    println!("First char: {}", first_char);
    println!("Second char: {}", second_char);
}

Because &str must always point to valid UTF-8 sequences, creating string slices using byte indices ([start..end]) has an important restriction: the start and end indices must fall on valid UTF-8 character boundaries. Attempting to create a slice where an index lies in the middle of a multi-byte character sequence is a runtime error and will cause your program to panic (a controlled crash indicating a program bug).

For the simpler examples in this chapter introducing slices, we often use ASCII text where each character is conveniently one byte long, making byte indices align with character boundaries. When working with text that may contain multi-byte characters, slicing using direct byte indices requires careful validation; often, iterating over characters or using methods designed for UTF-8 processing is a safer approach than direct byte-index slicing. Operations that could break the UTF-8 invariant (like arbitrary byte mutation within a &mut str) are also carefully controlled, as discussed later.

6.5.4 String Literals

Now we can understand string literals (e.g., "hello"). They are essentially string slices (&str) whose data is stored directly in the program’s compiled binary and is therefore valid for the entire program’s execution. Their type is &'static str, where 'static is a special lifetime indicating validity for the whole program runtime.

fn main() {
    let literal_slice: &'static str = "I am stored in the binary";
    println!("{}", literal_slice);
}

6.5.5 Slices in Functions

One of the most common uses for slices is in function arguments. Accepting a slice (&[T] or &str) instead of an owned type (like Vec<T> or String) makes a function more flexible and efficient, as it can operate on different kinds of data sources without taking ownership or requiring data copying.

// Function accepting an array/vector slice
fn sum_slice(slice: &[i32]) -> i32 {
    let mut total = 0;
    for &item in slice { // Iterate over elements in the slice
        total += item;
    }
    total
}

// Function accepting a string slice
fn first_word(text: &str) -> &str {
    // Iterate over bytes, find first space
    for (i, &byte) in text.as_bytes().iter().enumerate() {
        if byte == b' ' {
            return &text[0..i]; // Return slice up to space
        }
    }
    &text[..] // No space found, return whole slice
}

fn main() {
    // Array slice example
    let numbers = [1, 2, 3, 4, 5];
    // Can pass reference to array directly (coerces to slice)
    println!("Sum of numbers: {}", sum_slice(&numbers));
    // Or pass explicit slice
    println!("Sum of part: {}", sum_slice(&numbers[1..4]));

    // String slice example
    let sentence = String::from("hello wonderful world");
    println!("First word: {}", first_word(&sentence)); // Pass slice of String
    let literal = "goodbye";
    println!("First word: {}", first_word(literal)); // Pass a string literal directly
}

Note: Due to automatic deref coercions (discussed later), functions expecting &[T] can often directly accept references to arrays (&[T; N]) or Vec<T>s. Similarly, functions expecting &str can accept &String.

6.5.6 Exclusive Slices (&mut [T] and &mut str)

Exclusive slices (&mut [T]) allow modification of the elements within the borrowed sequence:

fn main() {
    let mut data = [10, 20, 30];
    let slice: &mut [i32] = &mut data[..];
    slice[0] = 15;
    slice[1] *= 2;
    println!("Modified data: {:?}", data); // Prints: [15, 40, 30]
}

Exclusive string slices (&mut str) exist but are more restricted. Because a &str (and &mut str) must always contain valid UTF-8, arbitrary byte modifications are disallowed. Furthermore, the length of a string slice cannot be changed, as this would require modifying the owner (e.g., reallocating a String), which a borrow cannot do. This prevents simple appending operations directly on a &mut str.

Exclusive string slices are primarily useful for in-place modifications that preserve UTF-8 validity and length, such as changing case via methods like make_ascii_uppercase(). For operations that need to change string length or might temporarily invalidate UTF-8, working directly with an owned String or an exclusive byte slice (&mut [u8]) is necessary.

fn main() {
    let mut s = String::from("hello");
    { // Limit scope of exclusive borrow
        let slice: &mut str = &mut s[..];
        slice.make_ascii_uppercase(); // In-place modification allowed
    } // Exclusive borrow ends here
    println!("Uppercase: {}", s); // Prints: HELLO
}

Remember that all slice operations must respect the borrowing rules – particularly the exclusivity of exclusive borrows for potentially overlapping data.


6.6 Lifetimes: Ensuring References Remain Valid

Rust lifetimes, those '_ things, are not directly about the liveness scope of values or variables, nor are they about when a value gets destructed. Instead, they are primarily about the duration of borrows. They are a compile-time concept, a type-level property that gets discarded after borrow checking completes and is not present during runtime. Variables themselves do not inherently have “Rust lifetimes” (those '_ things); rather, these lifetimes parameterize types, especially references.

Every reference in Rust has a lifetime, but the compiler can often infer them without explicit annotation through a set of rules called lifetime elision rules. You only need to write lifetime annotations when the compiler’s inference rules are insufficient to guarantee safety. This is always the case for structs containing references and is also required for functions where the relationship between input and output reference lifetimes is ambiguous.

6.6.1 Explicit Lifetime Annotation Syntax

When you need to be explicit, lifetime annotations use the following syntax:

Lifetime annotations do not change how long any values live. Instead, they describe the relationships between the validity scopes (lifetimes) of different references, allowing the borrow checker to verify that references are used safely. They act as constraints for the compiler’s analysis.

Example: Function with Lifetimes

Consider a function that returns the longer of two string slices. Because the returned reference borrows from one of the inputs, the compiler needs explicit annotations to know how the lifetime of the output relates to the lifetimes of the inputs.

// `<'a>` declares a generic lifetime parameter `'a`.
// `x: &'a str` and `y: &'a str` constrain both input slices to live at least
// as long as `'a`.
// `-> &'a str` declares that the ret. slice is also bound by this 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 string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        // The compiler enforces that 'a is, at most, the shorter borrow duration
        // of string1 and string2 relevant to this call.
        result = longest(&string1, &string2);
        println!("The longest string is '{}'", result); // Works here, result is valid.
    }
    // println!("The longest string is '{}'", result); // Compile-time error!
    // The borrow duration associated with `result` (which might point to `string2`'s
    // data) has ended because `string2` went out of scope. Using `result` here
    // would risk accessing freed memory.
}

The annotation 'a connects the lifetimes: the returned reference is guaranteed to be valid only as long as both input references (x and y) are valid. If the function tried to return a reference to data created inside the function (like the dangle example earlier), the compiler would reject it because that data’s lifetime would be shorter than the required lifetime 'a.

Important Clarification: When a function’s return type is annotated with a lifetime from an input (e.g., -> &'a str), it signifies that uses of the returned value keep the corresponding input borrow(s) active. It’s not that the lifetime of the return value is decided at the call site based on the inputs; rather, the borrow checker verifies that the returned reference’s actual use duration respects the longest possible duration implied by its inputs.

The 'static Lifetime

The special lifetime 'static indicates that a reference is valid for the entire duration of the program. String literals (&'static str) have this lifetime because their data is embedded in the program’s binary. References to global constants or leaked Boxes can also have the 'static lifetime.

Mastering lifetimes, particularly understanding elision rules and when annotations are needed, is key to leveraging Rust’s compile-time safety guarantees effectively. We’ll encounter more complex lifetime scenarios later.


6.7 Overview of Smart Pointers

In much of your Rust code, you’ll work with values stored directly on the stack or use standard library collections like Vec<T> and String, which manage their internal heap allocations automatically. However, Rust also provides smart pointers for specific situations requiring more explicit control over heap allocation, different ownership models (like shared ownership), or the ability to bypass certain borrowing rules safely (via runtime checks). Smart pointers are types that act like pointers but have additional metadata and capabilities, often related to ownership, allocation, or runtime checks. They provide abstractions over raw pointers for managing heap-allocated data or implementing these specific ownership patterns. Here’s a brief preview (detailed in Chapter 19):

These smart pointers offer different strategies for managing memory and ownership, providing flexibility beyond the basic rules while maintaining Rust’s safety guarantees (either at compile-time or runtime).


6.8 Unsafe Rust and C Interoperability

While Rust prioritizes safety, sometimes you need capabilities that the compiler cannot statically guarantee are safe. This is often required for low-level systems programming tasks (like interacting directly with hardware), optimizing performance-critical code, or interfacing with other languages like C that don’t share Rust’s guarantees. For these situations, Rust provides the unsafe keyword (detailed in Chapter 25).

6.8.1 unsafe Blocks and Functions

Inside an unsafe block or function, you gain access to five additional capabilities (“superpowers”) that are normally disallowed in safe Rust:

  1. Dereferencing raw pointers (*const T, *mut T).
  2. Calling unsafe functions or methods (including C functions via FFI and low-level intrinsics).
  3. Accessing or modifying mutable static variables.
  4. Implementing unsafe traits.
  5. Accessing fields of unions (unions require unsafe because Rust can’t guarantee which variant is active).
fn main() {
    let mut num = 5;

    // Creating raw pointers is safe (doesn't dereference)
    let r1 = &num as *const i32; // Immutable raw pointer
    let r2 = &mut num as *mut i32; // Mutable raw pointer

    // Dereferencing raw pointers requires an unsafe block
    unsafe {
        println!("r1 points to: {}", *r1); // Read via raw pointer
        *r2 = 10; // Write via raw mutable pointer
    }
    // Outside the unsafe block, normal rules apply again.

    println!("num is now: {}", num); // Prints: num is now: 10
}

Using unsafe signifies that you, the programmer, are taking responsibility for upholding memory safety for the operations within that block. The compiler trusts you to ensure that raw pointers are valid, functions uphold their contracts, etc. It’s crucial to minimize the scope of unsafe blocks and carefully document why they are necessary and correct. unsafe does not turn off the borrow checker entirely; it only enables these specific extra capabilities.

6.8.2 Interfacing with C (FFI)

Rust’s Foreign Function Interface (FFI) allows seamless calling of C code from Rust and exposing Rust code to be called by C. This involves using raw pointers and often unsafe blocks.

Calling C from Rust:

// Declare the C function signature using `extern "C"`
// This tells Rust to use the C Application Binary Interface (ABI).
// In Rust 2021+, extern blocks require `unsafe` if they contain functions.
unsafe extern "C" {
    fn abs(input: i32) -> i32; // Example: C standard library abs function
}

fn main() {
    let number = -5;
    // Calling external functions declared in `extern` blocks is unsafe
    let absolute_value = unsafe { abs(number) };
    println!("The absolute value of {} is {}", number, absolute_value);
}

Calling Rust from C:

Rust code compiled as a library (crate-type = ["cdylib"] or similar):

// Disable Rust's name mangling and use the C ABI
#[no_mangle]
pub extern "C" fn rust_adder(a: i32, b: i32) -> i32 {
    println!("Rust function called from C!");
    a + b
}

C code linking against the compiled Rust library:

#include <stdio.h>
#include <stdint.h> // For int32_t

// Declare the Rust function signature as it appears to C
extern int32_t rust_adder(int32_t a, int32_t b);

int main() {
    int32_t result = rust_adder(10, 12);
    printf("Result from Rust: %d\n", result); // Output: Result from Rust: 22
    return 0;
}

Tools like cbindgen (generates C/C++ headers from Rust code) and bindgen (generates Rust bindings from C/C++ headers) automate much of the boilerplate involved in FFI.


6.9 Comparison Summary: Rust vs. C Memory Management

Feature C / C++ (Manual/RAII) Rust (Ownership & Borrowing)
Memory Safety Prone to leaks, dangling ptrs, double frees, use-after-free, buffer overflows Compile-time prevention of these memory errors in safe code
Resource Mgmt Manual (free) or RAII (destructors) Automatic (Drop trait based on scope/ownership)
Data Races Possible via aliased mutable pointers (even single-threaded UB), or thread concurrency Prevented by borrow checking (shared/exclusive references), Send/Sync traits for threads
Pointers Raw pointers (*), potential null/invalid state/aliasing issues Safe references (shared/exclusive), guaranteed valid/non-null; raw pointers only in unsafe
Concurrency Requires manual locking/synchronization, error-prone Ownership/borrowing + Send/Sync provide compile-time concurrency safety
Runtime Overhead Minimal (manual) or depends on smart pointer/RAII logic Minimal (compile-time checks, Drop calls, slice bounds checks)
Flexibility High, but requires significant discipline for safety High, with safety by default; unsafe provides low-level control when needed

Rust’s ownership and borrowing system provides performance and control comparable to C/C++ while eliminating many common memory safety and concurrency pitfalls at compile time. This shifts bug detection much earlier in the development cycle.


6.10 Summary

This chapter introduced Rust’s core memory management philosophy, centered around ownership, borrowing, and lifetimes:

Mastering ownership, borrowing, and lifetimes is fundamental to writing effective, safe, and performant Rust code. It allows Rust to offer memory safety comparable to garbage-collected languages without the runtime overhead, making it highly suitable for the systems programming tasks familiar to C programmers.



| Contents |