Init keyword: even safer API around MaybeUninit

core::mem::MaybeUninit allows many useful patterns related to delayed and/or partial initialization. However, currently its safe subset of API doesn't play well with APIs like that of wasm_bindgen_futures::spawn_local and core::pin::Pin.

Assume that we have an Rc<MaybeUninit<T>> and we would like to initialize it using wasm_bindgen_futures::spawn_local. At the time of writing, we already can do that, yet we'd need to core::mem::transmute the clone of Rc<MaybeUninit<T>> to Rc<T> after initialization. However, what we really want to do is to provide a guarantee that the closure initializes the Rc<MaybeUninit<T>> and have a built-in safe mechanism to "properly initialize" the data.

The way I understand it, this idea of proper initialization is similar to core::marker::Send and core::marker::Sync. That is, a structure is properly initialized if all of its fields are properly initialized. Interaction with core::marker::PhantomData has to be explored but you can get the idea.

Then we can have init arguments/parameters.

fn initialize(init n: MaybeUninit<u32>) {
  n.write(42);
}

fn main() {
  let n =  MaybeUninit::<u32>::uninit();
  initialize(init n);
  // we assert that the type of `n` is `u32`
  let _ = core::convert::identity::<u32>(n);
}

As a bonus, init arguments/parameters would work even with core::pin::Pin!

We can have init work with types:

struct PartiallyInitialized {
  a: u32,
  b: Rc<MaybeUninit<u32>>,
}

// struct FullyInitialized {
//   a: u32,
//   b: Rc<u32>,
// }
type FullyInitialized = init PartiallyInitialized;

This is not enough to make init version of wasm_bindgen_futures::spawn_local because we would need Fn* traits aware of init and async effects. However, it would bring us a leap closer to that.

Since init is a popular identifier, perhaps it would be wise to make it a positional keyword.

This feature is similar to the unstable placement new feature that was removed from Rust a while back.

You mention Pin twice, yet don't explain anything about it, and I have a hard time reading your mind as to what kind of interaction with Pin you are imagining.

core::pin::Pin prevents the data from moving.

As a result of having of type Pin<MaybeUninit>, we have to resort to core::mem::transmute'ing the value after initialization, which is terribly unsafe. One place where core::pin::Pin appears is self-referential structs.

The example code with a simple let n = MaybeUninit::<u32>::uninit() is a bit useless, because obviously in this case, Rust’s static analysis for variable initialization can be used. It would be very helpful if you added code examples where the language feature you are imagining is actually useful, and even better an example that also has some good real-world motivation. You mention transmute-based workarounds, but of course, typically there are also safe workarounds involving run-time checks, e.g. with using Option instead of MaybeUninit. An argument for a new language feature would have to compare not only against the inconvenience or unsafety of existing unsafe solutions, but also against the (possibly small?) overhead of existing safe workarounds.


Assuming that the goal of this proposed language feature is to allow safe in-place construction of values, note that there are also some efforts for a library-based approach ([1] [2] [3]). You also refer to the placement-new feature, and while I’m personally not very familiar with what the design of that unstable feature looked like, if you say that what you propose here is similar, it’s probably important to look at the reason for what reasons the placement-new feature was removed, and whether the thing proposed here doesn't have the same shortcomings.

2 Likes

Note also that transmuting from Foo<MaybeUninit<Bar>> to Foo<Bar> is not always safe:

However remember that a type containing a MaybeUninit<T> is not necessarily the same layout; Rust does not in general guarantee that the fields of a Foo<T> have the same order as a Foo<U> even if T and U have the same size and alignment. Furthermore because any bit value is valid for a MaybeUninit<T> the compiler can’t apply non-zero/niche-filling optimizations, potentially resulting in a larger size:

assert_eq!(size_of::<Option<bool>>(), 1);
assert_eq!(size_of::<Option<MaybeUninit<bool>>>(), 2);

Is it documented somewhere that this is safe for Rc?

4 Likes

It's not; transmuting from Rc<MU<T>> to Rc<T> is relying on implementation details. The correct way to do the reinterpretation is Rc::from_raw(Rc::into_raw(rc) as *const T). from_raw permits this usage:

The raw pointer must have been previously returned by a call to Rc<U>::into_raw where U must have the same size and alignment as T. This is trivially true if U is T. Note that if U is not T but has the same size and alignment, this is basically like transmuting references of different types. See mem::transmute for more information on what restrictions apply in this case.

There's also the long unstable unsafe fn assume_init(Rc<MU<T>>) -> T that does this, but no guesses as to how long until that stabilizes.

9 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.