Search code examples
rustunsafe

Can `Layout::from_value(&v)` and `Layout::new::<T>` can ever be different where `v : T` in Rust?


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?


Solution

  • From the docs for std::mem::size_of_val:

    This is usually the same as size_of::<T>(). However, when T has no statically-known size, e.g., a slice [[T]][slice] or a [trait object], then size_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 :)