Chapter 17
Smart Pointers & Interior Mutability
When ownership, borrowing, and plain references aren't enough. Smart pointers give you heap allocation, shared ownership, runtime borrow checking, and copy-on-write semantics.
Box<T>, Rc<T>, Arc<T>
Box<T> - Heap Allocation
Box<T> puts a value on the heap and gives you an owned pointer to it. The value is dropped when the Box is dropped - same ownership semantics as a stack value, just stored elsewhere.
// Allocate a large struct on the heap instead of the stack
let config = Box::new(ServerConfig {
port: 8080,
workers: 4,
static_root: "/var/www".to_string(),
// ... many fields
});
// Access through the box - Deref makes it transparent
println!("Port: {}", config.port);
// Box is required for recursive types
enum Route {
Static(String),
Nested {
prefix: String,
children: Vec<Route>, // ✓ Vec already heap-allocates
},
Fallback(Box<Route>), // ✓ Box breaks the infinite size
}You need Box in three main situations:
- Recursive types - without
Box, the compiler can't calculate the size of a type that contains itself. - Trait objects -
Box<dyn Handler>stores any type implementingHandler. We've used this in our router since Chapter 10. - Large data - moving 10 KB on the stack is expensive. Boxing it means moves are just pointer copies.
Rc<T> - Shared Ownership (Single-Threaded)
Rc<T> (Reference Counted) allows multiple owners of the same data. Each Rc::clone increments a counter. When the last Rc is dropped, the data is freed:
use std::rc::Rc;
let shared_config = Rc::new(ServerConfig {
port: 8080,
workers: 4,
static_root: "/var/www".to_string(),
});
let config_for_router = Rc::clone(&shared_config); // refcount: 2
let config_for_logger = Rc::clone(&shared_config); // refcount: 3
println!("References: {}", Rc::strong_count(&shared_config)); // 3
drop(config_for_logger); // refcount: 2
// Data is freed when the last Rc is droppedRc is not thread-safe. Its reference count is a plain integer, not an atomic. The compiler enforces this - Rc does not implement Send, so you can't pass it to another thread. For multi-threaded sharing, use Arc.Arc<T> - Shared Ownership (Multi-Threaded)
Arc<T> (Atomic Reference Counted) is the thread-safe version of Rc. It uses atomic operations for the reference count, making it safe to share across threads:
use std::sync::Arc;
use std::thread;
let config = Arc::new(ServerConfig { port: 8080, /* ... */ });
let handles: Vec<_> = (0..4).map(|i| {
let config = Arc::clone(&config);
thread::spawn(move || {
println!("Worker {} using port {}", i, config.port);
})
}).collect();
for h in handles { h.join().unwrap(); }Choosing Between Them
| Type | Owners | Thread-safe | Cost |
|---|---|---|---|
| Box<T> | One | Yes (if T: Send) | Heap allocation only |
| Rc<T> | Many | No | Heap + refcount increment |
| Arc<T> | Many | Yes | Heap + atomic refcount |
RefCell<T> and Interior Mutability
Rust's borrowing rules are checked at compile time: either many &T or one &mut T. But sometimes you need to mutate data behind an immutable reference.RefCell<T> moves the borrow checking to runtime.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow - like &T
{
let view = data.borrow();
println!("Length: {}", view.len());
} // borrow ends here
// Mutable borrow - like &mut T
{
let mut writer = data.borrow_mut();
writer.push(4);
}
// These rules are checked at RUNTIME:
// - Multiple borrow() calls: OK
// - One borrow_mut() call: OK
// - borrow() and borrow_mut() at the same time: PANICSRefCell panics at runtime if you violate borrowing rules. It doesn't prevent bugs - it just defers the check. Use it only when you can't satisfy the compile-time rules. If your code panics on a borrow_mut(), the fix is usually to restructure the code so borrows don't overlap.RefCell is single-threaded (like Rc). For multi-threaded interior mutability, use Mutex or RwLock - which we covered in Chapter 12.
The Rc + RefCell Pattern
Rc<RefCell<T>> gives you shared, mutable data in single-threaded code. It's the closest Rust gets to a garbage-collected mutable object:
use std::cell::RefCell;
use std::rc::Rc;
// Shared mutable request log (single-threaded)
let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(vec![]));
let log_for_middleware = Rc::clone(&log);
let log_for_handler = Rc::clone(&log);
// Middleware writes to the log
log_for_middleware.borrow_mut().push("request received".to_string());
// Handler also writes to the log
log_for_handler.borrow_mut().push("response sent".to_string());
// Read the final log
println!("{:?}", log.borrow());
// ["request received", "response sent"]| Pattern | Threads | Borrow check |
|---|---|---|
| Rc<RefCell<T>> | Single | Runtime (panics) |
| Arc<Mutex<T>> | Multi | Runtime (blocks) |
| Arc<RwLock<T>> | Multi | Runtime (blocks, many readers OK) |
Cow<T> - Clone on Write
Cow<'a, T> (Clone on Write) is an enum that holds either a borrowed reference or an owned value. It defers cloning until mutation is needed - and if no mutation happens, the clone never occurs:
use std::borrow::Cow;
// Cow is defined as:
// enum Cow<'a, B: ToOwned + ?Sized> {
// Borrowed(&'a B),
// Owned(B::Owned),
// }
/// Normalize a path - only allocates if the path needs changing.
fn normalize_path(path: &str) -> Cow<'_, str> {
if path.ends_with('/') && path.len() > 1 {
// Need to modify - create an owned String
Cow::Owned(path.trim_end_matches('/').to_string())
} else {
// No modification needed - just borrow the original
Cow::Borrowed(path)
}
}
let a = normalize_path("/api/users"); // Cow::Borrowed - no allocation
let b = normalize_path("/api/users/"); // Cow::Owned - allocated a new String
// Both can be used as &str
println!("{} {}", a, b);Cow is perfect for functions that usually return the input unchanged but sometimes need to transform it. In a webserver, this applies to:
- Path normalization - most paths don't need changing.
- URL decoding - most URLs don't have encoded characters.
- Header canonicalization - most headers are already lowercase.
/// URL-decode a string. Only allocates if there's something to decode.
fn url_decode(input: &str) -> Cow<'_, str> {
if !input.contains('%') {
return Cow::Borrowed(input); // fast path - nothing to decode
}
let mut result = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push('%');
result.push_str(&hex);
}
} else if c == '+' {
result.push(' ');
} else {
result.push(c);
}
}
Cow::Owned(result)
}
let a = url_decode("hello"); // Borrowed - no %
let b = url_decode("hello%20world"); // Owned - decoded to "hello world"Cow implements Deref, so you can use it anywhere a &str or &[u8] is expected. It also implements Into<String> and Display, making it interchangeable with both borrowed and owned strings.Reference Cycles and Weak<T>
Rc and Arc have a flaw: if two values reference each other, their reference counts never reach zero and the memory is never freed. This is a reference cycle- a memory leak:
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: String,
next: RefCell<Option<Rc<Node>>>,
}
let a = Rc::new(Node { value: "A".into(), next: RefCell::new(None) });
let b = Rc::new(Node { value: "B".into(), next: RefCell::new(None) });
// Create a cycle: A → B → A
*a.next.borrow_mut() = Some(Rc::clone(&b));
*b.next.borrow_mut() = Some(Rc::clone(&a));
// When a and b go out of scope:
// a's refcount: 2 (variable 'a' + b.next) → drops to 1 → NOT freed
// b's refcount: 2 (variable 'b' + a.next) → drops to 1 → NOT freed
// Memory leak!Weak<T> solves this. A Weak reference doesn't count toward the reference count. It doesn't prevent the value from being dropped. You have to upgrade() it to an Rc/Arc to use it - and upgrade() returns None if the value has been dropped:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: String,
children: RefCell<Vec<Rc<Node>>>,
parent: RefCell<Option<Weak<Node>>>, // Weak - doesn't prevent cleanup
}
let parent = Rc::new(Node {
value: "parent".into(),
children: RefCell::new(vec![]),
parent: RefCell::new(None),
});
let child = Rc::new(Node {
value: "child".into(),
children: RefCell::new(vec![]),
parent: RefCell::new(Some(Rc::downgrade(&parent))),
// ^^^^^^^^^^^^^^ creates a Weak reference
});
parent.children.borrow_mut().push(Rc::clone(&child));
// Access parent from child:
if let Some(parent_ref) = child.parent.borrow().as_ref() {
if let Some(parent) = parent_ref.upgrade() {
println!("Parent: {}", parent.value);
} else {
println!("Parent has been dropped");
}
}The rule: strong references for ownership (parent → child), weak references for back-links (child → parent). This prevents cycles while still allowing navigation in both directions.
Arc pattern (no cycles) is all we need.Exercise
- Implement the
ServerStatepattern from this chapter in your server. Bundle config, stats, and the router into oneArc<ServerState>. Add aGET /statsendpoint that returns the request counts. - Write a
normalize_header_namefunction usingCow: if the name is already lowercase, return a borrow; if it contains uppercase letters, return an owned lowercase string. Benchmark it against an always-allocating version. - Replace the
Mutex<HashMap>in yourStore(from Chapter 14) with aRwLock<HashMap>. Why is this better for the todo API? When wouldMutexstill be the right choice? - Create a simple cache using
Arc<RwLock<HashMap<String, Weak<String>>>>. StoreArc<String>values elsewhere, andWeakreferences in the cache. When theArcis dropped, the cache entry'supgrade()returnsNone. This is a basic eviction strategy. - Use
Cowfor response bodies: if a handler returns a static string, useCow::Borrowed; if it builds a dynamic response, useCow::Owned. UpdateResponseto acceptCow<'static, str>.
What's Next
You now have the full toolkit for managing ownership beyond the basics: Box for heap allocation, Rc/ Arc for shared ownership, RefCell/ Mutex/RwLock for interior mutability, Cow for deferred cloning, and Weak for breaking cycles.
In the next chapter, we learn macros - Rust's metaprogramming system. We'll write a route! macro to make our router configuration cleaner and a json_response! macro to reduce response-building boilerplate.