36
Final Code The whole module in one block. The cost we paid for O(1) push_back: a raw-pointer layout, hand-written Drop, and the obligation to think about stacked borrows on every modification forever. The benefit: one allocation per push, no Box clone of the tail, no shared-ownership overhead. For a queue, it's the right trade.
01
The complete unsafe queue. Keep this on hand: in chapter 6 we generalize to a doubly-linked deque, where stacked borrows gets even fussier and we'll switch from raw pointers to NonNull<T> plus the same disciplines.
use std::marker::PhantomData;
use std::ptr;
pub struct List<T> {
head: *mut Node<T>,
tail: *mut Node<T>,
}
struct Node<T> {
elem: T,
next: *mut Node<T>,
}
pub struct Iter<'a, T> {
next: *const Node<T>,
_marker: PhantomData<&'a T>,
}
pub struct IntoIter<T>(List<T>);
impl<T> List<T> {
pub fn new() -> Self {
List { head: ptr::null_mut(), tail: ptr::null_mut() }
}
pub fn push_back(&mut self, elem: T) {
unsafe {
let new_tail = Box::into_raw(Box::new(Node {
elem,
next: ptr::null_mut(),
}));
if self.tail.is_null() {
self.head = new_tail;
} else {
(*self.tail).next = new_tail;
}
self.tail = new_tail;
}
}
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
if self.head.is_null() {
None
} else {
let head_box: Box<Node<T>> = Box::from_raw(self.head);
self.head = head_box.next;
if self.head.is_null() {
self.tail = ptr::null_mut();
}
Some(head_box.elem)
}
}
}
pub fn peek_front(&self) -> Option<&T> {
unsafe { self.head.as_ref().map(|n| &n.elem) }
}
pub fn peek_back(&self) -> Option<&T> {
unsafe { self.tail.as_ref().map(|n| &n.elem) }
}
pub fn is_empty(&self) -> bool {
self.head.is_null()
}
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head, _marker: PhantomData }
}
pub fn into_iter(self) -> IntoIter<T> { IntoIter(self) }
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<&'a T> {
unsafe {
self.next.as_ref().map(|node| {
self.next = node.next;
&node.elem
})
}
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> { self.0.pop_front() }
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
} TL;DR
Working O(1) singly-linked queue with raw-pointer head and tail. Pays the Box::into_raw/Box::from_raw allocator dance per push/pop and an explicit Drop; in return, one allocation per push and no aliasing of owned chains. Verified clean under Miri (stacked borrows + tree borrows).
End of Chapter V
Three perspectives on what you just read You came into this chapter knowing what a raw pointer costs and what UB looks like in C, and the chapter rewarded that. The queue's first draft compiled, ran, and was wrong, which is the same failure mode you have lived with for years under TBAA and __restrict__. The difference is that Rust's aliasing model, prototyped as Stacked Borrows and now evolving into Tree Borrows, is type-driven and enforced far more aggressively than C's effective-type rules — once an &mut T exists, every other access to that memory through a raw pointer is a violation, and the compiler is licensed to assume it. Miri executes this model on an interpreter, so the bug that LLVM happily miscompiled is the bug Miri prints a borrow stack for. The practical takeaway is the one the chapter hammered: do not interleave &mut and *mut to the same allocation. Pick raw pointers at the boundary, stay raw all the way through with Box::into_raw / Box::from_raw, and only re-enter the safe reference world once you are ready to honor its exclusivity guarantees.
# Py-track For readers from Ruby, Python, JavaScript If your background is Python, Ruby, or JavaScript, the most disorienting thing in this chapter is probably that the broken first version of the queue ran. It produced output. The tests passed. In the runtimes you are used to, a bug like this would have surfaced as an exception, a TypeError, a garbage collector assertion, or at worst a segfault you could trace — something would have stopped the program. Once you step outside safe Rust, that net is gone: "compiles and runs" is no longer a statement about correctness, only about syntax and luck. Undefined behavior means the optimizer was allowed to assume the rule you broke could not be broken, so the program you observe may share no relationship with the program you wrote. Miri is how you get a piece of that safety net back; it interprets your code against the actual aliasing rules and tells you the moment you violate them, which is the only reliable way to know whether your unsafe block is sound.
Take a moment to sit with what just happened, because it is genuinely strange. A raw pointer is just a number that says "the value you want lives at this address in memory, trust me" — there is no safety check, no one verifying the address is still valid, and no one stopping two parts of your program from poking at the same spot in incompatible ways. Undefined behavior, or UB, is what happens when your program breaks one of the rules the compiler was counting on; once that happens, the program is allowed to do anything at all, including appearing to work today and failing tomorrow. The unsettling part of this chapter is that the first version of the queue looked fine: it compiled, it ran, the tests passed, and it was still wrong in a way that could bite later. Miri is a tool that runs your program in slow motion and watches for exactly these broken rules, which is how the chapter found the bug that the normal compiler could not. The lesson worth keeping is small and concrete: when you write unsafe code, decide on one way of touching the memory and stick with it, because mixing the safe and unsafe styles is where the trouble starts.