Another Experiment To Make Unsafe Rust Safer: Preventing UB In MaybeUninit With Compile Time Error
I did some experiments, here are what I found:
The .write() can be made to take reference and return void not moving the self. And there will a new method named .init() that will init it, so the write does not always return new type. Only .init() will return new type and .write() reuse the returned type. But I can not find a way to make compile time error without making after init, it will return new type. Because the return different type is what make the compile time error occurs
I did find another way, this one allows to have same type. By using enum. The full code is like this :
use std::mem::MaybeUninit;
use std::marker::PhantomData;
pub struct Uninit<T>(MaybeUninit<T>);
impl<T> Uninit<T> {
pub fn new() -> Self {
Self(MaybeUninit::uninit())
}
pub fn zeroed() -> Self {
Self(MaybeUninit::zeroed())
}
fn init(&mut self, val: T) -> Init<T> {
self.0.write(val);
unsafe {
std::ptr::read(self as *mut Uninit<T> as *mut Init<T>)
}
}
}
pub struct Init<T>(MaybeUninit<T>);
impl<T> Init<T> {
pub fn ptr(&self) -> *const T {
self.0.as_ptr()
}
pub fn mut_ptr(&mut self) -> *mut T {
self.0.as_mut_ptr()
}
pub fn reff(&self) -> &T {
unsafe { self.0.assume_init_ref() }
}
pub fn mut_ref(&mut self) -> &mut T {
unsafe { self.0.assume_init_mut() }
}
pub fn assume_init(self) -> T {
let mut this = std::mem::ManuallyDrop::new(self);
unsafe { this.0.assume_init_read() }
}
pub fn replace(&mut self, val: T) -> T {
let old = unsafe { self.0.assume_init_read() };
self.0.write(val);
old
}
}
impl<T> Drop for Init<T> {
fn drop(&mut self) {
unsafe {
self.0.assume_init_drop();
}
}
}
enum Guard<T> {
Uninit(Uninit<T>),
Init(Init<T>)
}
pub struct UninitGuard<T> {
inner: Guard<T>
}
impl<T> UninitGuard<T> {
pub fn new() -> Self {
Self {
inner: Guard::Uninit(Uninit::new())
}
}
pub fn init(&mut self, val: T) {
if let &mut Guard::Uninit(ref mut inner) = &mut self.inner {
let new = inner.init(val);
self.inner = Guard::Init(new);
}
}
pub fn initialized_scope<U>(&mut self, closure: U) -> Result<(), &'static str>
where U: FnOnce(&mut Init<T>)
{
match &mut self.inner {
Guard::Init(inner) => {
closure(inner);
Ok(())
},
Guard::Uninit(_) => Err("not initialized, call init() first"),
}
}
}
But there is also cons. There is branching each time .initialized_scope() is called. If we can do multiple writes inside that scope, it oncly costs 1 branching for n writes. But if we can not do that, it costs n branchings that may negates the benefit of maybeuninit. Unsafe method with unreachable!() makro can be added to remove branching after it is guaranted that .init() os already called, but I can not find a way to make it only available for the enum variant Guard::Init(), so variant Guard::Uinit() can call it and cause UB
I also can not support array without creating new different type, because how many index has been written need to be saved somewhere that will be updated when calling .write_partial(). Trait only supports const not let so it can not be updated at runtime
Which one is better?
- compile time error, different type after init, write just reuse the type
- runtime check via branchings
And
- save written len inside the same struct using Option
- create different type for type that has len eg array
Discussion in the ATmosphere