IS4010: AI-Enhanced Application Development

Week 10: The Soul of Rust - Ownership & Borrowing

Brandon M. Greenwell

Session 1: The Ownership Model

Housekeeping: midterm project

  • Reminder: Your midterm project was due last week. If you haven’t submitted it yet, please reach out on Microsoft Teams
  • This week we’re diving into what makes Rust truly unique: its ownership system
  • This is the most important concept you’ll learn in Rust programming

Why ownership matters: a real-world crisis

The memory management trilemma

Historically, you had to pick two out of three:

  1. Performance (fast execution, low overhead)
  2. Safety (no crashes, no security vulnerabilities)
  3. Ease of use (simple mental model)

Traditional choices: - Python, Java: Safety + Ease → Garbage collector costs performance - C, C++: Performance + Ease → Manual memory management sacrifices safety

Rust’s innovation: Achieves all three through ownership!

How Python handles memory (garbage collection)

  • Python uses a garbage collector (GC) that runs in the background
  • The GC periodically scans memory to find objects that are no longer referenced
  • When it finds “garbage,” it automatically frees that memory
  • Pros: You never think about memory management
  • Cons: GC pauses slow down your program, uses extra memory, unpredictable timing

How C/C++ handles memory (manual management)

  • In C/C++, you are responsible for allocating and freeing memory
  • Use malloc()/new to allocate, free()/delete to deallocate
  • Pros: Maximum performance and control
  • Cons: Easy to make mistakes:
    • Use-after-free: Using memory after it’s been freed (security vulnerability)
    • Double-free: Freeing the same memory twice (crashes)
    • Memory leaks: Forgetting to free memory (resource exhaustion)

Rust’s approach: ownership

  • Rust introduces a third approach: the ownership system
  • The compiler enforces ownership rules at compile time
  • If your code violates these rules, it won’t compile
  • No runtime overhead (zero-cost abstraction)
  • No garbage collector needed
  • Memory safety guaranteed by the type system

The three ownership rules

These three rules are always enforced by the Rust compiler:

  1. Each value has a variable that’s called its owner
    • Every piece of data in memory has exactly one variable that owns it
  2. There can only be one owner at a time
    • When you assign a value to a new variable, ownership transfers (it “moves”)
  3. When the owner goes out of scope, the value is dropped

Ownership in action: the stack and the heap

  • Simple values like integers are stored on the stack (fast, fixed size, automatically managed)
  • Complex values like String are stored on the heap (flexible size, slower, needs management)
  • The stack holds the pointer, capacity, and length
  • The heap holds the actual string data
  • When ownership moves, the stack data moves, but heap data stays put (just the pointer changes)

Example: ownership transfer (move)

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership moves from s1 to s2

    // println!("{}", s1); // ❌ ERROR: value borrowed after move
    println!("{}", s2); // ✅ OK: s2 owns the string now
}

Try it in Rust Playground

Why moves matter: preventing double-free

  • If Rust allowed both s1 and s2 to be valid, both would try to free the same memory when they go out of scope
  • This is a double-free error - a serious security vulnerability
  • Rust prevents this by invalidating s1 after the move to s2
  • The borrow checker enforces this at compile time

The Copy trait: the exception to moves

  • Simple types like i32, f64, bool, and char implement the Copy trait
  • Types with Copy are copied instead of moved
  • This is efficient because they live entirely on the stack (no heap allocation)
  • After copying, both variables remain valid
let x = 5;
let y = x; // Copy happens (not a move)
println!("x = {}, y = {}", x, y); // ✅ Both valid!

🤖 AI co-pilot technique: understanding moves

When you encounter move errors, ask your AI assistant for help:

Effective prompts: - “Explain this Rust move error in simple terms: [paste error message]” - “Why does Rust move String but copy i32?” - “How do I use a value after it’s been moved?” - “What’s the difference between Copy and Clone in Rust?”

Pro tip: The Rust compiler error messages are incredibly helpful on their own - read them carefully before asking AI!

Scope and automatic cleanup

{
    let s = String::from("hello"); // s is valid here
    // use s
} // s goes out of scope, drop() is called automatically

Try it in Rust Playground

Session 2: Borrowing, References & Lifetimes

The problem: functions that take ownership

fn calculate_length(s: String) -> usize {
    s.len()
} // s is dropped here

fn main() {
    let my_string = String::from("hello");
    let len = calculate_length(my_string); // Ownership moved!
    // println!("{}", my_string); // ❌ ERROR: value borrowed after move
}
  • When we pass my_string to the function, ownership moves to s
  • After the function returns, we can’t use my_string anymore
  • This is annoying - we just wanted to read the length!

The solution: references and borrowing

  • A reference lets you refer to a value without taking ownership
  • References are created with the & operator
  • Using references is called borrowing
  • The original owner retains ownership, so the value won’t be dropped
fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but it doesn't own the String, so nothing is dropped

fn main() {
    let my_string = String::from("hello");
    let len = calculate_length(&my_string); // Borrow, don't move
    println!("{} has length {}", my_string, len); // ✅ Still valid!
}

Try it in Rust Playground

Immutable vs. mutable references

  • By default, references are immutable (&T)
  • You can create a mutable reference with &mut T
  • Mutable references let you modify the borrowed value
  • But there’s a catch… (next slide)
fn add_world(s: &mut String) {
    s.push_str(", world");
}

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

Try it in Rust Playground

The borrowing rules (enforced by the borrow checker)

The borrow checker enforces these rules at compile time:

  1. At any given time, you can have EITHER:
  2. References must always be valid

Why? These rules prevent data races at compile time!

Borrow checker error: multiple mutable references

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // ❌ ERROR: cannot borrow as mutable more than once

println!("{}, {}", r1, r2);

The error:

error[E0499]: cannot borrow `s` as mutable more than once at a time
  • Rust prevents multiple mutable references to avoid data races
  • A data race occurs when two threads access the same memory and at least one is writing
  • Rust’s rule is stricter: only one mutable borrow even in single-threaded code

Try it in Rust Playground

Borrow checker error: mixing mutable and immutable

let mut s = String::from("hello");

let r1 = &s;     // Immutable borrow
let r2 = &s;     // Another immutable borrow (OK)
let r3 = &mut s; // ❌ ERROR: cannot borrow as mutable

println!("{}, {}, {}", r1, r2, r3);

The error:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  • You can’t modify data while others are reading it
  • This prevents “reading while writing” bugs

Try it in Rust Playground

The fix: reference scope ends at last use

  • Non-Lexical Lifetimes (NLL) - a Rust 2018 improvement
  • A reference’s scope ends at its last use, not at the closing brace
  • This makes many common patterns “just work”
let mut s = String::from("hello");

let r1 = &s;     // Immutable borrow
let r2 = &s;     // Another immutable borrow
println!("{}, {}", r1, r2); // r1 and r2 last used here - scope ends

let r3 = &mut s; // ✅ OK: no immutable borrows are active
println!("{}", r3);

Try it in Rust Playground

🤖 AI co-pilot technique: debugging borrow checker errors

The borrow checker is your friend, but it takes practice. Use AI to accelerate your learning:

Effective prompts: - “Explain this borrow checker error: [paste full error message]” - “Why can’t I borrow this variable as mutable? [paste code]” - “How do I fix ‘cannot borrow as mutable more than once’?” - “What’s a dangling reference and how does Rust prevent them?”

Best practice: Read the error message first, understand the rule being violated, then ask AI for deeper insight or alternative solutions.

Preventing dangling references

  • A dangling reference is a pointer to memory that has been freed
  • This is a critical security vulnerability in C/C++ (use-after-free)
  • Rust makes dangling references impossible at compile time
fn dangle() -> &String {  // ❌ Won't compile!
    let s = String::from("hello");
    &s  // ERROR: s will be dropped, but we're returning a reference to it
} // s goes out of scope and is dropped, but the reference would point to freed memory

The fix: Return the owned value, not a reference:

fn no_dangle() -> String {  // ✅ Ownership transfers to caller
    let s = String::from("hello");
    s  // Move ownership out
}

Introduction to lifetimes

  • Lifetimes are Rust’s way of ensuring references are always valid
  • Every reference has a lifetime - the scope for which it’s valid
  • Usually, the compiler can infer lifetimes automatically
  • Sometimes you need to annotate them explicitly with 'a, 'b, etc.
  • Lifetimes prevent dangling references at compile time

Why lifetimes exist: a problematic example

fn longest(x: &str, y: &str) -> &str {  // ❌ Won't compile!
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The error:

error[E0106]: missing lifetime specifier
  --> src/main.rs:1:33
   |
1  | fn longest(x: &str, y: &str) -> &str {
   |               ----     ----     ^ expected named lifetime parameter
  • The compiler doesn’t know if the returned reference comes from x or y
  • It can’t verify the returned reference will be valid
  • We need to tell the compiler about the relationship between input and output lifetimes

Lifetime annotations: the solution

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • 'a is a lifetime parameter
  • It says: “the returned reference will live as long as the shortest-lived input”
  • If x lives longer than y, the return value’s lifetime is tied to y (the shorter one)
  • The borrow checker uses this to ensure safety

Try it in Rust Playground

When you need lifetime annotations

You typically need lifetime annotations when:

  1. Function returns a reference that came from one of multiple input references
  2. Structs contain references (struct must not outlive the referenced data)
  3. Multiple references with complex relationships where the compiler can’t infer

Good news: In most code, lifetimes are inferred automatically thanks to lifetime elision rules

🤖 AI co-pilot technique: understanding lifetimes

Lifetimes can be confusing. Use AI to build intuition:

Effective prompts: - “Explain Rust lifetimes like I’m coming from Python” - “Why does this code need a lifetime annotation? [paste code]” - “What does ‘a mean in this function signature? [paste signature]“ - ”How do I fix ’lifetime may not live long enough’?” - “When can I omit lifetime annotations in Rust?”

Pro tip: Use Rust Playground to experiment. Change code, see errors, ask AI to explain the errors.

Lifetimes in structs

struct Excerpt<'a> {
    text: &'a str,  // This reference must be valid as long as the Excerpt exists
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { text: first_sentence };
    println!("{}", excerpt.text);
} // excerpt and novel both go out of scope here (OK: excerpt doesn't outlive novel)
  • When a struct contains a reference, we must annotate the lifetime
  • 'a says: “this struct can’t live longer than the data it references”
  • The borrow checker enforces that novel outlives excerpt

Try it in Rust Playground

The big picture: why ownership matters

What ownership gives you: - Memory safety without garbage collection - No data races (guaranteed at compile time) - Predictable performance (no GC pauses) - Zero-cost abstractions (compile-time checks, zero runtime overhead)

The trade-off: - Steeper learning curve (you’re learning now!) - Fight the borrow checker (initially - then you’ll think differently) - More upfront thinking about data ownership

Career insight: Once you understand ownership, you’ll write better code in every language. You’ll think about memory and references more carefully even in Python or JavaScript.

Real-world performance wins

Examples of Rust’s ownership system delivering real results:

  • Discord: Switched from Go to Rust, reduced latency spikes from seconds to milliseconds (10x improvement)
  • Dropbox: Rewrote sync engine in Rust, reduced memory usage by 2x, improved performance
  • npm: Rewrote CPU-heavy services in Rust, 10x performance improvement
  • Microsoft: Using Rust in Windows to prevent 70% of security vulnerabilities

The common thread: No garbage collector + memory safety = high performance + security

Try it yourself: borrow checker challenges

Challenge 1: Fix the ownership error Rust Playground Link

Challenge 2: Fix the borrowing error Rust Playground Link

Challenge 3: Fix the dangling reference Rust Playground Link

Introducing Lab 10: ownership and borrowing mastery

This week’s lab has two parts:

  1. The Borrow Checker Game
    • Fix 5-7 progressively challenging ownership and borrowing errors
    • Learn to interpret compiler error messages
    • Practice the fix-compile-test cycle
  2. Hands-on Ownership Exercises
    • Implement functions demonstrating ownership concepts
    • Work with references and borrowing patterns
    • Test your understanding with cargo test

Full instructions: labs/lab10/README.md

Resources for going deeper

Official Rust resources: - The Rust Programming Language book - Chapter 4 (Ownership) - The Rust Programming Language book - Chapter 10 (Lifetimes) - Rust by Example - Ownership - The Rustonomicon - Advanced ownership

Interactive learning: - Rust Playground - experiment in your browser - Rustlings - small exercises to get you used to reading and writing Rust code

Community: - r/rust - Rust subreddit - The Rust Programming Language Discord

Looking ahead: next week

Week 11 preview: structs, enums, and pattern matching - Creating custom data types with struct - Rust’s powerful enum type - Pattern matching with match - AI-assisted data modeling strategies

For now: Focus on mastering ownership and borrowing. These concepts underpin everything else in Rust!

Questions?

Remember: - The borrow checker is your friend (even when it feels like your enemy) - Compiler error messages are incredibly helpful - read them carefully - Use AI assistants to deepen understanding, not to skip learning - Fighting with Rust now makes you a better programmer forever

Office hours: Available on Microsoft Teams - reach out anytime!