External Publication
Visit Post

Another Experiment To Make Unsafe Rust Safer: Preventing UB In MaybeUninit With Compile Time Error

Rust Internals [Unofficial] May 23, 2026
Source

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

Loading comments...