My linked list has this memory layout:
[dummy] -> (prev, ?DUMMY?, next) <-> (prev, A, next) <-> (prev, B, next)
^ ^
+---------------------------------------------------------+
The dummy node is lazily initialized upon insertion, with prev and next pointer pointing towards itself. After insertion, the prev and next pointer will act as the tail and head pointer.
use std::marker::PhantomData;
use self::node::NodePtr;
mod node;
#[derive(Debug)]
pub struct LinkedList<T> {
dummy: Option<NodePtr<T>>,
len: usize,
_phantom: PhantomData<T>,
}
impl<T> Default for LinkedList<T> {
fn default() -> Self {
Self {
dummy: None,
len: 0,
_phantom: PhantomData,
}
}
}
impl<T> LinkedList<T> {
pub fn new() -> Self {
Default::default()
}
pub(crate) fn inner(&mut self) -> NodePtr<T> {
*self.dummy.get_or_insert(NodePtr::dummy())
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn push_front(&mut self, elem: T) {
let dummy = self.inner();
let head = dummy.next();
let new_head = NodePtr::alloc(dummy, elem, head);
head.set_prev(new_head);
dummy.set_next(new_head);
self.len += 1;
}
pub fn pop_front(&mut self) -> Option<T> {
let dummy = self.inner();
unsafe { dummy.next().dealloc() }.map(|(_, elem, new_head)| {
dummy.set_next(new_head);
new_head.set_prev(dummy);
self.len -= 1;
elem
})
}
}
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
let dummy = self.dummy.take().unwrap();
unsafe {
let _ = Box::from_raw(dummy.as_ptr());
}
}
}
node.rs
use std::{
fmt::Debug,
mem::MaybeUninit,
ops::Not,
ptr::{self, NonNull},
};
#[derive(Debug)]
pub struct Node<T> {
prev: NodePtr<T>,
next: NodePtr<T>,
elem: MaybeUninit<T>,
}
pub struct NodePtr<T> {
ptr: NonNull<Node<T>>,
}
impl<T> Debug for NodePtr<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodePtr").field("ptr", &self.ptr).finish()
}
}
impl<T> Clone for NodePtr<T> {
fn clone(&self) -> Self {
Self { ptr: self.ptr }
}
}
impl<T> Copy for NodePtr<T> {}
impl<T> PartialEq for NodePtr<T> {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self.as_ptr(), other.as_ptr())
}
}
impl<T> Eq for NodePtr<T> {}
impl<T> NodePtr<T> {
pub unsafe fn dangling() -> Self {
Self {
ptr: NonNull::dangling(),
}
}
pub unsafe fn raw_alloc(prev: Self, elem: MaybeUninit<T>, next: Self) -> Self {
let ptr = Box::into_raw(Box::new(Node { prev, next, elem }));
let ptr = NonNull::new_unchecked(ptr);
Self { ptr }
}
pub fn alloc(prev: Self, elem: T, next: Self) -> Self {
unsafe { Self::raw_alloc(prev, MaybeUninit::new(elem), next) }
}
pub fn dummy() -> Self {
unsafe {
let dangling = Self::dangling();
let dummy = Self::raw_alloc(dangling, MaybeUninit::uninit(), dangling);
dummy.set_prev(dummy);
dummy.set_next(dummy);
dummy
}
}
pub fn prev(self) -> Self {
unsafe { (*self.as_ptr()).prev }
}
pub fn set_prev(self, ptr: Self) {
unsafe {
(*self.as_ptr()).prev = ptr;
}
}
pub fn next(self) -> Self {
unsafe { (*self.as_ptr()).next }
}
pub fn set_next(self, ptr: Self) {
unsafe {
(*self.as_ptr()).next = ptr;
}
}
pub fn is_dummy(self) -> bool {
self.prev() == self.next() && self.prev() == self
}
pub fn as_ptr(self) -> *mut Node<T> {
self.ptr.as_ptr()
}
pub unsafe fn as_ref<'a>(self) -> &'a Node<T> {
self.ptr.as_ref()
}
pub unsafe fn as_mut<'a>(mut self) -> &'a mut Node<T> {
self.ptr.as_mut()
}
pub fn get_raw(self) -> Option<NonNull<T>> {
self.is_dummy()
.not()
.then(|| unsafe { NonNull::new_unchecked((*self.as_ptr()).elem.as_mut_ptr()) })
}
pub unsafe fn get<'a>(self) -> Option<&'a T> {
self.get_raw().map(|ptr| ptr.as_ref())
}
pub unsafe fn get_mut<'a>(self) -> Option<&'a mut T> {
self.get_raw().map(|mut ptr| ptr.as_mut())
}
pub unsafe fn dealloc(self) -> Option<(Self, T, Self)> {
self.is_dummy().not().then(|| {
// println!("Deallocating...");
let Node { prev, next, elem } = *Box::from_raw(self.as_ptr());
(prev, elem.assume_init(), next)
})
}
}
Running the test cases with miri:
#[cfg(test)]
mod test {
use super::{node::NodePtr, LinkedList};
#[test]
fn test_node() {
let dummy = NodePtr::dummy();
let node = NodePtr::alloc(dummy, 100, dummy);
dummy.set_next(node);
dummy.set_prev(node);
let (_, elem, _) = unsafe { node.dealloc() }.unwrap();
unsafe {
Box::from_raw(dummy.as_ptr());
}
assert_eq!(elem, 100);
}
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// Try to break an empty list
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// Try to break a one item list
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// Mess around
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
}
miri output:
The following memory was leaked: alloc86121 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f2da8[a86121]<205762> (8 ptr bytes)╼ ╾0x1f2da8[a86121]<205762> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc86346 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f3ef0[a86346]<206225> (8 ptr bytes)╼ ╾0x1f3ef0[a86346]<206225> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc86565 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f4ff8[a86565]<206712> (8 ptr bytes)╼ ╾0x1f4ff8[a86565]<206712> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc86723 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f5c58[a86723]<207063> (8 ptr bytes)╼ ╾0x1f5c58[a86723]<207063> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc86946 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f6da8[a86946]<207524> (8 ptr bytes)╼ ╾0x1f6da8[a86946]<207524> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc87169 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f7f60[a87169]<207985> (8 ptr bytes)╼ ╾0x1f7f60[a87169]<207985> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc87393 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1f9100[a87393]<208448> (8 ptr bytes)╼ ╾0x1f9100[a87393]<208448> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc87599 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1fa1b0[a87599]<208910> (8 ptr bytes)╼ ╾0x1fa1b0[a87599]<208910> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc87823 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1fb2f8[a87823]<209373> (8 ptr bytes)╼ ╾0x1fb2f8[a87823]<209373> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc88030 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1fc3a8[a88030]<209837> (8 ptr bytes)╼ ╾0x1fc3a8[a88030]<209837> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc88237 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1fd3a8[a88237]<210301> (8 ptr bytes)╼ ╾0x1fd3a8[a88237]<210301> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc88454 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1fe4e8[a88454]<210788> (8 ptr bytes)╼ ╾0x1fe4e8[a88454]<210788> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc88613 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1ff160[a88613]<211141> (8 ptr bytes)╼ ╾0x1ff160[a88613]<211141> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
alloc88773 (Rust heap, size: 24, align: 8) {
0x00 │ ╾0x1ffe18[a88773]<211496> (8 ptr bytes)╼ ╾0x1ffe18[a88773]<211496> (8 ptr bytes)╼ │ ╾──────╼╾──────╼
0x10 │ __ __ __ __ __ __ __ __ │ ░░░░░░░░
}
I got 14 memory leaks but running the test_node case alone doesn't leak any memory. With some further debugging, I can assure that NodePtr::dealloc does run properly ("Deallocating" got printed for 5 times as expected. Note that this method won't be deallocating the dummy node, and I would deallocate the dummy node manually in the Drop impl). The code logic seems to work correctly. Could it be some kind of subtle undefined behavior?