Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
222: Implementing a Doubly Linked List with Unsafe Rust
I've always found that the best way to wrap your head around raw pointers is to build a doubly linked list. It's the classic "final boss" of memory management. In safe Rust, a doubly linked list is a nightmare because the borrow checker sees the circular references—where a node points to the next and the next points back to the previous—as a violation of ownership rules. You could use Rc<RefCell<T>>, but that adds runtime overhead and feels like you're fighting the language.
Today, we're going to go "off-road" with unsafe. We'll build a simple Playlist manager. The goal is to have a list of song titles that we can traverse in both directions. To do this, we'll use NonNull, which is essentially a wrapper around a raw pointer that tells Rust the pointer will never be null, allowing the compiler to optimize Option<NonNull<T>> to be the same size as a single pointer.
Defining the Node and the List
First, we need a Node. Since we're managing memory manually, we can't rely on standard references. We need raw pointers for both the next and prev links.
use std::ptr::NonNull;
use std::marker::PhantomData;
struct Node<T> {
val: T,
next: Option<NonNull<Node<T>>>,
prev: Option<NonNull<Node<T>>>,
}
pub struct Playlist<T> {
head: Option<NonNull<Node<T>>>,
tail: Option<NonNull<Node<T>>>,
len: usize,
_marker: PhantomData<T>,
}
I added PhantomData here. Because the Playlist struct only holds raw pointers, Rust doesn't actually "know" that Playlist owns T. PhantomData tells the compiler to treat the struct as if it owns T, which ensures the correct variance and drop-check behavior.
Adding the first song
Now, let's implement push_front. This is where we enter the unsafe block. We have to allocate memory on the heap using Box::new, then immediately turn that box into a raw pointer using Box::into_raw. This tells Rust: "Stop managing this memory; I've got it from here."
impl<T> Playlist<T> {
pub fn new() -> Self {
Self { head: None, tail: None, len: 0, _marker: PhantomData }
}
pub fn push_front(&mut self, val: T) {
let new_node = Box::new(Node {
val,
next: self.head,
prev: None,
});
let node_ptr = NonNull::new(Box::into_raw(new_node)).unwrap();
unsafe {
if let Some(mut old_head) = self.head {
old_head.as_mut().prev = Some(node_ptr);
} else {
self.tail = Some(node_ptr);
}
self.head = Some(node_ptr);
}
self.len += 1;
}
}
The "missing link" bug
When I first wrote this logic a few years ago, I made a classic mistake. I updated the head of the list, but I forgot to update the prev pointer of the node that used to be the head. I'd push three items, and then try to traverse backward from the tail, only to realize I could only go back one step before hitting a None.
If you look at the code above, the if let Some(mut old_head) = self.head block is the fix. It ensures that the existing list is properly linked back to the new node. Without that line, you don't have a doubly linked list; you just have a singly linked list with some useless prev fields.
Cleaning up the heap
Here is the most dangerous part. Since we used Box::into_raw, Rust's automatic memory management is disabled for these nodes. If our Playlist goes out of scope, we have a massive memory leak. We must implement the Drop trait to manually walk the list and reclaim the memory.
impl<T> Drop for Playlist<T> {
fn drop(&mut self) {
while let Some(node_ptr) = self.head {
unsafe {
let node = Box::from_raw(node_ptr.as_ptr());
self.head = node.next;
// node is dropped here as it goes out of scope
}
}
}
}
Notice how I use Box::from_raw. This effectively "re-claims" the memory from the raw pointer and puts it back into a Box. Once that Box goes out of scope at the end of the while loop iteration, Rust's normal cleanup kicks in and the memory is freed. It's a delicate dance, but it's the only way to ensure our Playlist doesn't eat all the system RAM.
📋 Practical Task
Implement the push_back Method for Playlist
Now that you've seen how push_front works, your task is to implement the push_back method for the Playlist<T> struct. This method should add a new node to the end of the list and correctly update the tail pointer and the next pointer of the previous tail node.
Requirements:
- Use
Box::into_rawto allocate the new node. - Correctly handle the case where the list is currently empty (both
headandtailshould point to the new node). - Ensure the previous tail's
nextpointer is updated to point to the new node. - Increment the
lenof the playlist.
There are no comments for now.