I'm implementing my own Box<T>
from standard library to learn how to write unsafe code.
My implementation looks something like:
use std::marker::PhantomData;
use std::{alloc, mem, ptr};
use ptr::NonNull;
use alloc::Layout;
pub struct MyBox<T> {
data: NonNull<T>,
_marker: PhantomData<T>,
}
impl<T> MyBox<T> {
pub fn new(data: T) -> MyBox<T> {
unsafe {
let layout = Layout::for_value(&data);
let ptr = alloc::alloc(layout) as *mut T;
ptr::write(ptr, data);
let ptr = NonNull::new(ptr).unwrap_or_else(|| alloc::handle_alloc_error(layout));
MyBox {
data: ptr,
_marker: PhantomData,
}
}
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
let ptr = self.data.as_ptr();
drop(ptr::read(ptr));
alloc::dealloc(ptr as *mut u8, Layout::new::<T>())
}
}
}
As you can see, I use different methods to obtain the layout, however, dealloc
requires us to use the same layout which was used to allocate the value.
According to the source code of those methods, Layout::for_value
uses mem::size_of_val
and mem::align_of_val
while Layout::new
uses mem::size_of
and mem::align_of
. As far as I could grasp from the documentation, they can only be different if T is a dynamically-sized type (DST). So as far as I implement MyBox
for sized types only (and they actually are unless explicitly specified) I should be fine, shouldn't I?
From the docs for std::mem::size_of_val
:
This is usually the same as
size_of::<T>()
. However, whenT
has no statically-known size, e.g., a slice [[T]
][slice] or a [trait object], thensize_of_val
can be used to get the dynamically-known size.
By default, when you introduce a type parameter, T
, as you've done in MyBox<T>
, there is an implicit T: Sized
constraint. This means that both mem::size_of_val
and mem::size_of
will return the same thing.
In order to support unsized T
, you need to make that explicit:
pub struct MyBox<T> where T: ?Sized {
// ...
}
And:
impl<T> Drop for MyBox<T> where T: ?Sized {
fn drop(&mut self) {
// ...
}
}
Note that your new
function still needs to have T: Sized
because it needs to accept it as an argument by value. You may however find other ways to construct a MyBox
for DSTs.
With this change, you now can't use Layout::new<T>()
, because it requires T: Sized
. Instead, you'll need to construct the Layout
using for_value
, which doesn't have the Sized
constraint.
pub struct MyBox<T: ?Sized> {
data: NonNull<T>,
_marker: PhantomData<T>,
}
impl<T: ?Sized> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
let ptr = self.data.as_ptr();
let layout = Layout::for_value(unsafe { &*ptr } );
ptr.drop_in_place();
alloc::dealloc(ptr as *mut u8, layout)
}
}
}
You also need to change drop(ptr::read(ptr));
to ptr.drop_in_place()
- partly because it's better, but mostly because drop
requires T: Sized
too :)