I try to create an unsafe, but more performant ArrayQueue
implementation. After I added test cases, one of them produces a segmentation fault.
Here is my simple minimal implementation:
use std::mem;
pub struct ArrayQueue<T> {
buff: Vec<T>,
head: usize,
size: usize,
}
impl<T> ArrayQueue<T> {
pub fn new(size: usize) -> Self {
let mut buff = Vec::with_capacity(size);
unsafe {
buff.set_len(size);
}
ArrayQueue {
buff: buff,
head: 0,
size: 0,
}
}
pub fn add(&mut self, elem: T) {
let idx = (self.head + self.size) % self.buff.len();
*unsafe { self.buff.get_unchecked_mut(idx) } = elem;
self.size += 1;
}
pub fn remove(&mut self) -> T {
let idx = self.head;
self.size -= 1;
self.head = (self.head + 1) % self.buff.len();
mem::replace(unsafe { self.buff.get_unchecked_mut(idx) }, unsafe {
mem::uninitialized()
})
}
}
impl<T> Drop for ArrayQueue<T> {
fn drop(&mut self) {
let mut idx = self.head;
for _ in 0..self.size {
// Drop only valid elements of the queue
drop(unsafe { self.buff.get_unchecked_mut(idx) });
idx = (idx + 1) % self.buff.len();
}
unsafe {
// Prevent deallocation of vector elements
// This still dallocates vector's internal buffer
self.buff.set_len(0);
}
}
}
#[cfg(test)]
mod test {
use super::ArrayQueue;
#[test]
fn test0() {
let mut x = ArrayQueue::new(10);
x.add(String::from("K"));
assert_eq!(x.remove(), String::from("K"));
}
#[test]
fn test1() {
let mut x: ArrayQueue<Box<String>> = ArrayQueue::new(10);
x.add(Box::new(String::from("K")));
assert_eq!(x.remove(), Box::new(String::from("K")));
}
}
I believe I am doing proper dropping to prevent any memory leaks.
I've attached two test cases where one works, but the other results in a crash due to invalid memory reference.
It crashes inside the add
method (*unsafe {self.buff.get_unchecked_mut(idx)} = elem;
) and I suspect that happens because I am somehow trying to write to an invalid memory location.
I've specifically used heap allocated objects for vector elements in test but to my surprise String
works properly while Box
doesn't.
I would like to understand if it's possible to make an unsafe implementation like this and why it's currently failing?
Edit
I've fixed the issue by replacing *unsafe {self.buff.get_unchecked_mut(idx)} = elem;
with unsafe {std::ptr::write(self.buff.get_unchecked_mut(idx), elem)};
Now I would like to understand why this works and previous version doesn't
When you run *unsafe { self.buff.get_unchecked_mut(idx) } = elem;
to replace an uninitialized Box
or String
, it will run drop
on this uninitialized Box
or String
. Box
and String
both contain a pointer to the part of the heap where their data is supposed to be stored, and when they are dropped, it will deallocate the memory at this location.
By dropping an uninitialized Box
or String
, it will be deallocating memory at an arbitrary location as the uninitialized pointer could be anything. It is undefined behavior to deallocate memory which has not been allocated.