{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreid4w6vjibuc7hecyrcp42gcp2dl5ngswqacevo64vzb2nfbf4kcfi",
"uri": "at://did:plc:ivbknywyskln22er3nkssdhl/app.bsky.feed.post/3mmjtmp3iua72"
},
"path": "/t/another-experiment-to-make-unsafe-rust-safer-preventing-ub-in-maybeuninit-with-compile-time-error/24352#post_9",
"publishedAt": "2026-05-23T15:25:44.000Z",
"site": "https://internals.rust-lang.org",
"textContent": "I did some experiments, here are what I found:\n\nThe .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\n\nI did find another way, this one allows to have same type. By using enum. The full code is like this :\n\n\n use std::mem::MaybeUninit;\n use std::marker::PhantomData;\n\n pub struct Uninit<T>(MaybeUninit<T>);\n impl<T> Uninit<T> {\n pub fn new() -> Self {\n Self(MaybeUninit::uninit())\n }\n\n pub fn zeroed() -> Self {\n Self(MaybeUninit::zeroed())\n }\n\n fn init(&mut self, val: T) -> Init<T> {\n self.0.write(val);\n unsafe {\n std::ptr::read(self as *mut Uninit<T> as *mut Init<T>)\n }\n }\n }\n\n pub struct Init<T>(MaybeUninit<T>);\n impl<T> Init<T> {\n pub fn ptr(&self) -> *const T {\n self.0.as_ptr()\n }\n\n pub fn mut_ptr(&mut self) -> *mut T {\n self.0.as_mut_ptr()\n }\n\n pub fn reff(&self) -> &T {\n unsafe { self.0.assume_init_ref() }\n }\n\n pub fn mut_ref(&mut self) -> &mut T {\n unsafe { self.0.assume_init_mut() }\n }\n\n pub fn assume_init(self) -> T {\n let mut this = std::mem::ManuallyDrop::new(self);\n unsafe { this.0.assume_init_read() }\n }\n\n pub fn replace(&mut self, val: T) -> T {\n let old = unsafe { self.0.assume_init_read() };\n self.0.write(val);\n old\n }\n }\n\n impl<T> Drop for Init<T> {\n fn drop(&mut self) {\n unsafe {\n self.0.assume_init_drop();\n }\n }\n }\n\n enum Guard<T> {\n Uninit(Uninit<T>),\n Init(Init<T>)\n }\n\n pub struct UninitGuard<T> {\n inner: Guard<T>\n }\n\n impl<T> UninitGuard<T> {\n pub fn new() -> Self {\n Self {\n inner: Guard::Uninit(Uninit::new())\n }\n }\n\n pub fn init(&mut self, val: T) {\n if let &mut Guard::Uninit(ref mut inner) = &mut self.inner {\n let new = inner.init(val);\n self.inner = Guard::Init(new);\n }\n }\n\n pub fn initialized_scope<U>(&mut self, closure: U) -> Result<(), &'static str>\n where U: FnOnce(&mut Init<T>)\n {\n match &mut self.inner {\n Guard::Init(inner) => {\n closure(inner);\n Ok(())\n },\n Guard::Uninit(_) => Err(\"not initialized, call init() first\"),\n }\n }\n }\n\n\nBut 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\n\nI 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\n\nWhich one is better?\n\n * compile time error, different type after init, write just reuse the type\n * runtime check via branchings\n\n\n\nAnd\n\n * save written len inside the same struct using Option\n * create different type for type that has len eg array\n\n",
"title": "Another Experiment To Make Unsafe Rust Safer: Preventing UB In MaybeUninit With Compile Time Error"
}