---
title: "Design meeting 2025-07-30: In-place initialization"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2025-07-30
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-07-30/
url: https://hackmd.io/XXuVXH46T8StJB_y0urnYg
---
# In-place initialization design meeting
Thanks for coming to the in-place init design meeting. Before reading this document, please start by reading the project goal proposal:
[rust-lang/rust-project-goals#344](https://rust-lang.github.io/rust-project-goals/2025h2/in-place-initialization.html)
You don't need to read the proposals linked in the goal; just the goal itself. The proposals are summarized later in this doc.
## Motivation
In-place initialization is when a value is constructed in-place. Generally, this means that the function takes an out-pointer, and writes the value to that pointer.
But why would you want to do that?
### Avoiding stack overflows
When you write code such as
* `Box::new(MyLargeStruct::new())`
* `runtime.spawn(my_large_future())`
your program may crash with a stack overflow. This is because the "straight-line" execution of the code is this:
1. Create `MyLargeStruct::new()` on the stack.
2. Allocate memory to hold the value.
3. Move the value into the allocation.
To optimize this into something where `MyLargeStruct::new()` is created directly on the heap, the optimizer must swap steps 1 and 2. Optimizers find it really difficult to prove that swapping 1 and 2 have no side effects, so they usually don't.
With in-place initialization, the straight-line execution of the code is
1. Allocate memory to hold the value.
2. Create `MyLargeStruct::new()` directly on the heap.
So we get the behavior we want without relying on the optimizer to do something it finds difficult to do.
#### The code ordering problem
Avoiding stack overflows is the simplest use-case of in-place initialization, but it highlights one challenge with it. There is a tension between:
1. I want to write code that looks like `Box::new(MyLargeStruct::new())`, but
2. I want `Box::new` to run code *before* `MyLargeStruct::new()` runs any code.
And in more complex cases such as `vec.push(MyStruct::new())`, you want `Vec::push` to run code both before *and* after the call to `MyStruct::new`.
### Returning pinned values
This is a topic that comes up often in relation to C/C++ interop, so I will start by discussing that. However, as seen below, it is not specific to C/C++ interop and can be useful in pure Rust as well.
C and C++ types commonly cannot be relocated in memory or require extra code (a "move constructor") in order to relocate. Today, Rust types are typically relocatable using only a `memcpy` of their bytes into a new location. Unfortunately, most types in C++ are not `memcpy`-relocatable, and need to be pinned to a particular location in memory.
The same issue comes up with C interop in the Linux Kernel. For example, its mutex has this constructor:
```c
void mutex_init(struct mutex *mutex);
```
That is, initialization happens via an out-pointer, and the resulting `struct mutex` is *not* movable with `memcpy` because the Linux kernel mutex is self-referential. Another example is
```c
int misc_register(struct miscdevice *misc);
```
where the resulting value is not movable because it is added to a global linked list of all miscdevices. Note also that `misc_register` is an example of a fallible constructor, as it may return an error code.
#### Example: small string optimization
In discussions of Rust/C++ interop, it is often said that if you can support `std::string`, then you can support any C++ value.
One common implementation of `std::string` is the small-string optimization where the length/capacity fields are reused to store the actual string data, and the pointer points directly inside the struct itself:

This implementation, used in libstdc++ (the GCC implementation of the C++ standard library), is useful because the pointer *always* points at the string data no matter which implementation is used, so getting a pointer to the string data is doable without any branches.
The disadvantage of this implementation is that the string is not trivially relocatable. Moving the string moves the string data, so the pointer must be updated to point at the new location.
### Custom self-referential structs
However, returning pinned values is not just useful for C/C++ interop. It will also enable the language to support custom self-referential structs. Consider this code:
```rust
struct MyStruct {
x: u32,
y: &'self u32,
}
impl !Unpin for MyStruct {}
impl MyStruct {
pub fn new() -> MyStruct {
MyStruct {
x: 10,
y: &x,
}
}
}
```
This kind of code doesn't work today because the address of `x` changes as `MyStruct` is returned from `new`. Furthermore, the user could move the struct after calling `new`, changing the address of `x` again. In-place initialization solves the first part, and the additional feature of returning pinned values solves the second part.
### Returning unsized values (or async fn in dyn Trait)
When values are returned using in-place initialization, it is usually not a problem if the value is unsized. Both the init expressions proposal by Alice and the placement by return RFC (which mirrors C++) directly address how they can support unsized return values, and I think that no matter which solution we choose, it will not be difficult to support unsized return values.
And once we support unsized return values, it becomes easy to support language features such as `async fn` in `dyn Trait`. For example, with the init expressions proposal, the syntax for calling an async function on a trait object is:
```rust
Box::init(my_trait.my_async_fn()).await
```
The proposal supports storing the future in any container that supports initializers, so you might also use a solution that stores it on the stack if the future is small, and otherwise boxes it. Or you might use a [reusable box](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.ReusableBoxFuture.html):
```rust
reusable_box.init(my_trait.my_async_fn()).await;
```
#### How does unsized return values work?
The init expressions proposal defines this trait:
```rust
unsafe trait Init<T: ?Sized + Pointee> {
type Error;
/// The layout needed by this initializer.
fn layout(&self) -> Layout;
/// Writes a valid value of type `T` to `slot` or fails.
unsafe fn init(self, slot: *mut ()) -> Result<T::Metadata, Self::Error>;
}
```
This allows `Box::init` to perform these steps:
1. Query the requested layout.
2. Allocate memory.
3. Call `init`.
To support `async fn` in dyn trait, the compiler generates the code below. When you call `<dyn MyTrait>::foo` it immediately returns a `FooInit` as defined below:
```rust
// Given this trait
trait MyTrait {
fn foo(&self, arg1: i32, arg2: &str) -> impl MyOtherTrait;
}
// The compiler generates this struct
struct FooInit<'a> {
// the fields are just the arguments to `<dyn MyTrait>::foo`
r#self: &'a dyn MyTrait,
arg1: i32,
arg2: &'a str,
}
impl Init<dyn MyOtherTrait> for FooInit<'_> {
type Error = Infallible;
fn layout(&self) -> Layout {
// layout of concrete return type stored in vtable
self.r#self.vtable.foo_layout
}
fn init(self, slot: *mut ()) -> Result<T::Metadata, Infallible> {
self.r#self.vtable.foo_fn(
slot,
self.r#self as *const _,
self.arg1,
self.arg2
)
}
}
```
Note that this usually implies that `dyn Trait` will *not* implement `Trait` when such mechanisms are used.
## Features of a solution
Different solutions have different trade-offs, and one important trade-off is whether to support certain use-cases or not. This section goes through important features that I think a solution should consider.
### Composition
If I have `MyStruct` with an in-place constructor, can I use it as a field in another struct?
```rust
struct MyOtherStruct {
x: MyStruct,
y: MyStruct,
z: i32,
}
```
Supporting this implies some sort of safe way to define an in-place constructor for `MyOtherStruct` that calls the in-place constructor for `MyStruct` twice.
### Fallible initialization
Constructors may not be guaranteed to succeed. In the Linux Kernel, the constructor may return an integer representing success or an error code. In C++ interop, the constructor could throw an exception, which we may wish to translate into a `Result` on the Rust side.
It is not enough to just in-place construct a `Result` value. The discriminant must be returned separately somehow. For example, if we consider `MyOtherStruct` from the previous section, there is no space for the `Result` discriminant when constructing `MyOtherStruct`. And maybe the `Result` error type is larger than `MyStruct`.
There is flexibility here. Do you only support `Result`? Do you support any `Try` type? Do you try to be even more general and support cases such as `Either<A,B>` where both `A` and `B` are returned with in-place initialization?
### Returning pinned values
For some use-cases such as C/C++ interop or custom self-referential types, the ability to return pinned values from constructors is critical. For others, such as async fn in dyn Trait, it is not required.
### Returning unsized values
When the return value is written to an out-ptr, it's usually possible to support unsized return values with minimal additional effort.
### Async support
I don't think we should design a new in-place initialization language feature that is incompatible with async/await. But none of the use-cases in the motivation section require it.
Note that there is a potential soundness issue here, but I think it is solvable. We do not need to spend time on it here.
### Const support
As seen previously, creating a `Mutex` in the Linux Kernel requires in-place initialization. How do you support in-place initialization in this case?
```rust
static MY_MUTEX: Mutex<i32> = Mutex::new(0);
```
what about this case?
```rust
struct MyStruct {
mutex: Mutex<i32>,
data: i32,
}
static MY_VALUE: MyStruct = MyStruct {
mutex: Mutex::new(0),
data: 42,
};
```
## Factors of a solution
When considering `Box::new(MyLargeStruct::new())`, each solution needs to consider three different end-user experiences:
* Implementing `MyLargeStruct::new()`.
* Calling `MyLargeStruct::new()`.
* Implementing `Box::new` or `Vec::push`.
I think this is also an area with significant opportunity for *combining* solutions. One solution may provide good syntax for implementing `MyLargeStruct::new()`, and another solution may provide good syntax for calling `MyLargeStruct::new()`. There is a good chance that you can get the best of both worlds.
## What real-world projects do today
There are a few real world projects that have implemented their own version of in-place initialization in external crates using macros:
* The Linux Kernel and its [pin-init] crate.
* Crubit and its [Ctor] trait for C++ interop.
* The [moveit] crate, also for C++ interop.
[pin-init]: https://github.com/rust-for-linux/pin-init
[moveit]: https://docs.rs/moveit/latest/moveit/new/trait.New.html
[Ctor]: https://github.com/google/crubit/blob/c65afa7b2923a2d4c9528f16f7bfd4aef6c80b86/support/ctor.rs#L189-L226
These crates generally rely on the same approach:
1. Define a new trait for an "initializer".
2. Use a macro for composing initializers (fields -> struct).
3. Add new methods to `Box` and similar to call an initializer. (And a macro for initialization on the stack.)
### The code ordering problem
As mentioned earlier, there is tension between:
* I want to write code that looks like `Box::new(MyLargeStruct::new())`, but
* I want `Box::new` to run code *before* `MyLargeStruct::new()` runs any code.
Generally, all existing third-party implementations solve this tension like this:
1. Have `MyLargeStruct::new()` return an initializer, which is a type of closure.
2. Define a new `Box::new_with` method instead of using `Box::new`.
3. Call the initializer/closure inside `Box::new_with`.
### Composition
In general, third-party implementations of in-place initialization support composition in much the same way as well.
```rust
// pin-init
#[pin_init]
struct MyStruct {
nice_rust_type: u32,
#[pin]
c_type: CType,
}
impl MyStruct {
fn new() -> impl PinInit<MyStruct> {
pin_init!(MyStruct {
nice_rust_type: 42,
c_type: CType::new(),
})
}
}
```
```rust
// crubit
#[ctor::recursively_pinned]
struct MyStruct {
nice_rust_type: u32,
cpp_type: CppType,
}
emplace!{
let x = ctor::ctor!(MyStruct {
nice_rust_type: 42,
cpp_type: CppFunction(),
});
}
```
That is, there is a macro that lets you use the usual struct initializer syntax, but the fields are allowed to be an initializer. The macro usually evaluates to an anonymous type that implements the initializer trait.
### Initializer trait
Although third-party solutions all treat initializers as a kind of closure and define a trait for them, they disagree on the shape of the trait.
To compare the Linux Kernel and Crubit:
```rust
pub unsafe trait PinInit<T: ?Sized, E = Infallible>: Sized {
/// Initializes `slot`.
///
/// # Safety
///
/// - `slot` is a valid pointer to uninitialized memory.
/// - the caller does not touch `slot` when `Err` is returned, they are only permitted to
/// deallocate.
/// - `slot` will not move until it is dropped, i.e. it will be pinned.
unsafe fn __pinned_init(self, slot: *mut T) -> Result<(), E>;
}
```
```rust
pub unsafe trait Ctor: Sized {
/// The constructed output type.
type Output: ?Sized;
/// The error type if initialization fails. This should typically
/// be `Infallible`, as there's only limited support for real
/// errors currently.
type Error;
/// Constructs a value in place.
///
/// Before this call, `dest` is uninitialized. After this call,
/// if `ctor` does not panic, and returns `Ok`, then `dest` is
/// initialized to the constructed value.
///
/// # Safety
///
/// `dest` is valid for writes, pinned, and uninitialized.
unsafe fn ctor(self, dest: *mut Self::Output) -> Result<(), Self::Error>;
}
```
The primary difference is whether the target type is an associated type, or a generic parameter.
## Proposals
The previous section shows what third-party solutions do when they are constrained by having to implement everything with macros. What can we do if it's a language feature?
### Init expressions
This is [the proposal by Alice](https://hackmd.io/@aliceryhl/BJutRcPblx), which essentially just takes the approach of the pin-init crate in the Linux kernel and turns it into a langauge feature.
The idea is to add a new kind of closure:
```rust
init MyStruct {
field1: initer_for_field1,
field2: 0,
}
```
which is a more convenient version of the macro in the pin-init crate. These initializer expression can be quite powerful and allow you to write essentially any initializer you might want. On the other hand, they have the disadvantage of moving all of the logic into a closure-like construct, and making this fact explicit to the end-user.
With this solution, the code ordering problem is solved by adding a trait for initializers. Types such as `Box` must provide a separate constructor `Box::init` that take an `impl Init`.
#### Values are initializers
One idea in this proposal is to include the following blanket impl:
```rust
impl<T> Init<T> for T {
fn init(self, slot: *mut T) {
slot.write(self);
}
}
```
the idea is that any type can be initialized by providing an instance of that type. In principle this means that we could implement a `Box` where the *only* constructor is one that takes an initializer, since it still lets you do `Box::new(value)` and have it work with the initializer through the blanket impl. However, this would be a breaking change due to weakening type inference.
This approach is only possible if `Init` uses a generic rather than associated type for the target type, as otherwise the blanket impl prevents manual implementations. Not using an associated type does have other disadvantages.
### Return Value Optimization AKA do what C++ does
This is the [placement by return RFC](https://github.com/rust-lang/rfcs/pull/2884) from 2020 by Olivier Faure. The idea is that instead of providing a new trait for initializers, we just make *all* functions initializers. That is, given any closure, you have the option to provide an out pointer when calling it.
Furthermore, when declaring a function, it will *automatically* initialize the value returned as the last expression using this out pointer. That is, syntax like this:
```rust
fn foo() -> MyStruct {
MyStruct {
field: [0; 1024],
another_field: 20,
}
}
```
is guaranteed to be returned by directly writing the two fields directly to the out pointer when `foo` is called using the new out-ptr mechanism.
The guarantee only applies to the last expression, so this:
```rust
fn foo() -> MyStruct {
// Not last expression, so in-place init not used.
let x = MyStruct {
field: [0; 1024],
another_field: 20,
};
x
}
```
might write `x` to the stack and then copy it to the out pointer afterwards.
This still does not directly solve the code ordering problem. The RFC proposes to add an `Box::new_with` method that takes an initializer of type `impl FnOnce() -> T`. Then, the implementation of `Box::new_with` uses the new method call mechanism to provide an out-ptr when calling the closure.
### Placing Functions
This is [the proposal by Yosh](https://blog.yoshuawuyts.com/placing-functions/), which reasons about emplacement as a generator-like effect. It uses a declarative annotation that can be added to existing functions, which doesn't require changes to the signature. **The express goal of this proposal is to enable the stdlib to gradually opt-in to placing semantics, just like we're currently doing with `const fn`:**
```rust
#[placing] // ← 1. Marks a function as "placing".
fn new_cat(age: u8) -> Cat { // ← 2. Has a logical return type of `Cat`.
Cat { age } // ← 3. Constructs `Cat` in the caller's frame.
}
```
Internally [this desugars][placing-desugaring] to an out-pointer, similar to [C++'s deferred temporary materialization][cpp-nrvo] and [the placement-by-return proposal][pbr]. But instead of being applied to _all_ code on a flag day (risky), functions defintions can individually opt-in using an annotation. Function calls don't require any annotations, keeping usage patterns identical:
[placing-desugaring]: https://github.com/yoshuawuyts/placing
[cpp-nrvo]: https://devblogs.microsoft.com/cppblog/guaranteed-copy-elision-does-not-elide-copies
[pbr]: https://hackmd.io/yhf9bi9TQauaiaYG6bt3OA?both#Return-Value-Optimization-AKA-do-what-C-does
```rust
let cat = new_cat(12); // ← `Cat` is constructed in-place here.
assert_eq!(cat.age(), &12);
```
This design also plays particularly nice with ["super let"-based temporary lifetime extensions](https://blog.m-ou.se/super-let/). `super let` as implemented today only allows values to be emplaced in parent block scopes. With placing functions we would be able to extend `super let` to emplace in function callers' scopes as well:
```rust
#[placing] // ← 1. Marks a function as "placing".
fn new_cat(age: u8) -> Cat { // ← 2. Has a logical return type of `Cat`.
super let cat = Cat { age } // ← 3. Constructs `Cat` in the caller's frame.
cat // ← 4. Logically return the emplaced `Cat`.
}
```
There is more to say about the direction this design takes us in, including: ['super lifetimes][super-lt], [placing arguments][place-args], interactions with effects like `try` and `async`, [emplacing `Pin<&mut T>` constructors][pin-emplace], and more. All of that builds on the same declarative, opt-in annotation we've seen so far. And crucially: it can be gradually introduced without breaking compatibility.
[super-lt]: https://blog.yoshuawuyts.com/placing-functions/#what-about-borrows-local-lifetime-extensions
[place-args]: https://blog.yoshuawuyts.com/placing-functions/#what-about-placing-arguments
[pin-emplace]: https://blog.yoshuawuyts.com/placing-functions/#what-about-pinning
### In-place initialization via outptrs
The [In-place initialization via outptrs](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q) proposal by Taylor Cramer points out that the init expressions proposal is quite limited when it comes to control flow. This is a natural consequence of moving everything into explicit closures. In her proposal, Taylor suggests that functions could use syntax along these lines:
```rust
out.field = [0; 1024];
out.another_field = 20;
```
and then at the end you return `out`.
This syntax would allow you to initialize (and deinitialize) the fields in whichever order you want as long as all fields are initialized when the function returns `out`.
This solution is indeed quite flexible with respect to return types. You might return `Ok(out)` in the fallible case, or return `out` in the infallible case, or return `Some(out)` in the `Option` case. The proposal shows signatures that look like this:
```rust
fn make_x_in_place1(out: Uninit<'_, X>) -> InPlace<'_, X> { ... }
fn make_x_in_place2(out: Uninit<'_, X>) -> Option<InPlace<'_, X>> { ... }
fn make_x_in_place3(out: Uninit<'_, X>) -> Result<InPlace<'_, X>, Error> { ... }
```
where `Uninit` serves as the out pointer, and `InPlace` is a marker type that represents a proof that `out` has been initialized.
### Combining solutions
I could imagine combining the above solutions. The idea is that we still introduce some sort of initializer trait like the one from my proposal, and we still have a `Box::init` method that takes an initializer trait. But we allow people to use syntax like this to implement initializers:
```rust
fn make_x_in_place() -> init MyStruct {
out.field = [0; 1024];
out.another_field = 20;
out
}
// sugar for
fn make_x_in_place() -> impl Init<MyStruct> {}
```
```rust
fn make_x_in_place() -> Result<init MyStruct, Error> {
out.another_field = 20;
if foo() {
// another_field is implicitly dropped
return Err(err);
}
out.field = [0; 1024];
Ok(out)
}
// sugar for
fn make_x_in_place() -> impl Init<MyStruct, Error> {}
```
And the compiler knows that when a signature contains `init` in the return type, then it needs to convert it into a function that returns an initializer. Similar to async/await, the body is not executed until you call the initializer.
I think it is probably possible to improve the `Init` trait to also support the `Option` case, making the final solution flexible wrt. signatures like Taylor's proposal.
## Notes from meeting
This section is where we take notes from meeting discussion. While you read the document, feel free to edit this section to add questions you would like to raise during the meeting.
### How do I add a question?
Ferris: To add a question, create a section (`###`) like this one. That helps with the markdown formatting and gives something to link to. Then you can type your name (e.g., `ferris:`) and your question. During the meeting, others might write some quick responses, and when we actually discuss the question, we can take further minutes in this section.
---
# Discussion
## Attendance
- People: Alice Ryhl, Benno Lossin, Xiang, TC, Eric Holk, Josh Triplett, Tomas Sedovic, Tyler Mandry, Amanieu, Yoshua Wuyts, Taylor Cramer, Olivier Faure, Niko Matsakis
## Meeting roles
- Driver: TC
- Minutes: Tomas
## Let's hear it from Alice
Alice: What do you think about the project goal? I asked you to read the project goal at the beginning of the doc. Design axiom: take advantage of the language. There's multiple crates that exist today. So if we're introducing this in the language, what can we do better beyond macros?
Tyler: I'm open to using language features. I'd suggest phrasing the design axiom in terms of what do the language features buy us. It seems they'd buy us ergonomics and more safety. I think it's important we have both (as safe as they can be). Using lang featuresn to selve that is totally worth it.
Niko: What are we looking to gain by making this a language feature? One of the things I wanted form the doc was: what are the various goals and which ones to prioritize?
Taylor: I did make a list of some of the proposals that were lang features: init expressions, async fn in dyn trait returning an impl object, box new constructors (though that's more t-libs), gradual structure initialization, uninit references and syntax for having in-place return values.
Alice: The init expressions proposal I originally put out -- one thing I thought recently was: I've been working with less experienced Rust developers and they struggled with the init macros. That's why I put this combining solution at the end. Maybe we could do better.
Niko: Taylor, that's a reallly heplful list. What are the usecases and impact and what's going to be using it? Alice's insight helped a lot -- expecting not just Rust wizards using this.
Yosh: All three of us had different designs, spoke to each other, arrived at different points. So this is us agreeing of all the problems and putting them together. E.g. Alice had a list of features she needed and I needed none of these. My hope is getting the sennse of the problem. Sense of the features that we'd need. Agree with the lang team that we even need a solution. Care about backwards compatibility.
Niko: Can you ellaborate what you mean on backwards compatibility?
Yosh: We take the const approach. We gradualy constified the std. And we improved const to be more powerful. We could do the same gradual process for emplacement. Start with simple things and then supporting more and more things going forward.
TC: How do you distinguish that from "all code continues to compile"? On the constification case the reason we did it gradually so all code continued to compile.
Niko: Not only does code continue to compile but all code is idiomatic.
Taylor: I think the difference is the scope of code that does const initialization. Every fn we expect to become a `const fn` at some point. But I don't think we can expect that for emplacement. I don't think it's a goal to have all Rust code emplacing.
Olivier: Going back to about trying to find good points in each feature. I don't have a position I'm trying to push. If we forget stick to cases where we don't have to return options and results etc. we only need some form of return emplacement and initialization and we can get all the benefits that have been mention in Alice's document. We can get a long way with return value initialization.
Taylor: Any version of a return-value initialization will not give you a result type and any generic type.
Olivier: Yes, if we omit fallible emplacement, placement value is enough. But since we can't, we need to have a deeper discussion.
Olivier: My second point is thiat. What we're trying to do is create an object, start with unitialized blob and at some point flick a switch, and aggregation of uninitialized bytes and values becomes an initialized value with a destructor and that guarantees that it respects its invariants and so on. The most intuitive way to do that is via the existing struct initialization syntax. Any concept that has new forms of constructors, doing partial initialization is ending up more complicated.
Josh: :+1:, enthusiastically in support of the project goal, and in support of trying an experiment. I think we have copious evidence now, thanks to Alice and others, that there *is* value in integrating this as a language feature. Doing so would improve usability, and in particular take advantage of things the compiler can do, like knowing whether a struct has been fully initialized (which we *already* have the logic to check).
If we have the proposed hybrid feature, which integrates with named return values, could we then easily support this in closures in addition to functions? Is that an expected part of the proposal?
Broadly speaking, my primary remaining concern would be the way in which the `Init` trait interacts with error handling and similar control flow. If we're going to support arbitrary functions with named return values, we clearly need *some* solution for that, but the current proposal does seem specialized on `Result` and *only* `Result`. That said, I think we should give this project goal the go-ahead, and ask that the experiment explore the possibilities there.
Alice: The idea of that part of the doc is if you write something in a certain way it'll get converted into impl Init just like async returns impl Future.
Josh: Is there anything that would prevent closures using that too?
Alice: I think so. There may be issues with variable shadowing.
Josh: Yes. But semantically in the language, is there anything that would block us form doing init closures?
Alice: I'm sure you can do this.
Josh: Okay, that seems to me that supersedes needing init expressions.
Taylor: I think that's pretty close to a proposal I had. You can imagine either using init expressions or something I had. It would actually provide an initializer.
Josh: I just wanted to at a high level wanted to make sure that it covers closures and functions since the examples are all functions. The other concern with the init trait being heavily connect to returning a `Result`. But that's can be explored.
Alice: Olivier was talking about the C++ proposal where most of the other proposals hardcode Results/Options. Taylor's is the only one that doesn't do that.
Taylor: The C++ is the easiest piece. In general C++ constructors can't return errors, don't have other control flows. They just initialize the value into a pointer. Any of these proposals can do that. Question is: if we're doing this in Rust, can we do something more flexible and ergonomic?
Tyler: I added a question about this below.
Beno: Olivier, was your point that if we don't support fallibility, then we already get most things form just using return value optimization?
Olivier: Yes.
Benno: But I think fallible initialization is something that we really want to do. If we want to have this as a language feature then all the crates in the ecosystem should be able to use this feature. There are other things we have to have inside the lang feature: const support and and zeroable support.
Olivier: I'm not saying we could do without fallible initialization. Just trying to point out that since we have something that return value optimization get's pretty close we should look at that and see how we can add fallible initialization on top. If we start with that we don't necessarily have to add that many features to support the usecases we want.
Niko: Might be helpful to have a chart of all the capabilities cross-references with usecases that require them. For fallible return Taylor's version sounds different. When you return the Result of the final version -- you want the out pointer to have the Ok type.
Taylor: The impl init step is a step that can fail so if you nest the impl init inside a result. So you can't know whether the result is ok or err until you've run the impl init inside. There's also an access to dyn-compatibily. I think other proposals aren't dyn-compatible.
Tyler: I wanted to talk about all these axes (Taylor just started to type one at the end of the doc). How are we going to arrive at this proposal and waht ar the qualities/attributes that we want? One of them is: how much should we care about the more general case than `Result`. We know we want to support `impl Iniit<T>` and `Result<T>` and there are more general cases. How much should we wait for that particular usecase?
Alice: I think there's a really big gap from supporting one out pointer and one with multiple different out pointers. I think we can probably all support Result and Option. Possibly even support a tuple. But supporting multiple out pointers becomes a different story.
Tyler: You're saying that dramatically changes the set of acceptable designs?
Alice: Yeah. I coded the out parameter out. In Rust for Linux pretty much all the out pointer cases are constructors so there it makes sense to just call the out parameter `self`.
Tyler: It would be great to see motivation for usecases that aren't `T` and `Result<T>` and `Option<T>`.
Alice: In the linux kernel the out pointer is just a pointer and the result is just an integer.
Benno: I don't know any use cases that wouldn't use `Result`.
Taylor: There are a lot of usecases that are infallible.
Benno: That's just a `Result<.., !>`.
(discussion about how that's unergonomic)
Niko: I don't think we'll ever do this to treat result with a ! type as a non-result value.
Josh: We need to write a value to a pointer. And at the end we need to know whether a value was written to the pointer or whether there was an error. Part of the problem is having some way to write the return type in a way that includes a placeholder for "in this variant you have the type initialized". You could use `&mut T` for that, but...
Taylor: That's what my proposal does. It doesn't use `&mut T`, it uses `inplace T`.
Josh: That seems really appealing, since you could then use that with `Result`, `Option`, maybe even `impl Future<Output = inplace T>`...
TC: I want to echo the vibe Niko put out here. It would be unfortunate to bake the `Result` here in the same way we'd almost done for `Future` -- not doing that was the right call. Another option might be a custom type for this; we do that in some places, e.g. `Poll`, `ControlFlow`, `CoroutineState` (unstable).
TC: Alice, are you getting what you need out of this meeting? Are there questions you need to see answered from us in this meeting to get you unblocked.
Alice: The thing about hardcoding result is useful to me. One thing I wanted to bring up was a type that's a "hole" (there's a type missing -- see section below). It could be a hole, or a result with a hole, or a tuple of a hole. And when you call init it fills out the hole with `Box<T>` or an `Arc<T>` etc. But we don't have this mechanism right now.
Josh: Part of the issue is you have to mark it somehow that you're not return the value. You need to mark that the value is valide.
Josh: Is there anybody on the team with an objection with the project goal and experiment going forward? Is everyone comfortable with all these questions being answered by the project goel / experiment?
Olivier: I'm not in the lang team. But it doesn't feel we did much to discriminate between the approaches here so far. I don't know how you see the project goal being worked on but it sousnds like an expectation is Alice or someone is going to go on implementing one proposal and will use that as a basis for the design -- whether that's something we want. But I don't thikn this meeting has established which of these proposals would be the one that would be implemented in the project goal. Unless the plan is to implement all three of them?
Taylor: I think we should multiple of these in some sense.
Amanieu: I proposed as a project goal a MIR move elimination optimization that would cover a lot of what Yosh and Olivier are propozing. It will optimize move away in local functions. IT covers eliminatinng copies when passing and returning values.
Taylor: That only covers when there's a unique place.
Amanieu: It's an optimization and to do tihs soundly you need to do some liveness analysis. Downside: it's an optimization not a guarantee. May not do this in debug builds depending on performance. It does'nt provide any language guarantees.
Tyler: I'm feeling partial to an approach that has clear execution and makes use of some natural exensions to the language. Which makes me favor the out pointer proposal Taylor made. Can we make that much more ergonomic similar to what Yosh laid out. And if we can do that, how do we handle async functions ??. If we can do that, I'd be very happy.
Benno: To what Amanieu said -- we definitely need a guarantee. In the kernel we use them for self-referential types. After they're initialized, they need to be pinned and any fields need to also be pin-initialized. So we definitely can't rely on optimization guarantees taht sometimes may not happen. We also want to make it explicit and then get a type error that can point you to the issue.
Amanieu: I agree. I don't think we can guarantee this with an optimization. There fore I think this project goal should ofcus on something that can support self-referential types that the initializer can run once you know the final address. Everything else could be done as an optimization but not this.
Olivier: In what I and Yosh are proposing these would be language features with explicit rules with which items you're able to annotate in which positions. If you create a variable this way, you have a guarantee that it will always be gauranteed that it will be inatilized this way and that the compiler will not be using any moves. With that you can do self-referential varibales.
Alice: One thing I've realised with the C++ way it works is: they call all this optimizations but they **guarantee**. I think C++ has another one where you create the value and you can move it, it
Taylor: NRVO is maybe what you mean but calls to std::move are preserved.
Niko: I really would like to see the table and I'd love to iterate on the things like async. But I have a preference for somewhere in between. Where we have a generit feature but that can ..
## Naming the output value
cramertj: The example in Alice's proposal above reads:
```rust
fn make_x_in_place() -> Result<init MyStruct, Error> {
out.another_field = 20;
if foo() {
// another_field is implicitly dropped
return Err(err);
}
out.field = [0; 1024];
Ok(out)
}
// sugar for
fn make_x_in_place() -> impl Init<MyStruct, Error> {}
```
There is a similar option suggested [at the end of my outptr doc](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#Ergonomics-extension-in-place-return-values):
```rust
fn make_my_struct() -> in_place { out: MyStruct } {
out.x = make_x_in_place(&uninit out.x);
out.y = &raw const out.x;
out
}
let my_struct: MyStruct;
my_struct = make_my_struct(out = &uninit my_struct);
```
There are a couple differences here: naming of the output value, and structure of the resulting initializer.
On naming: in Alice's suggestion above, the `out` value is implicitly created after seeing `init` in the return type. The name `out` is "magic", and only one outptr can appear in any function signature. IMO it would be nice to allow users to introduce the name here.
nikomatsakis: I had a similar question about the initial proposal for `-> init Foo` and `init Foo {..}` syntax. I think the entire point is that the init Foo expression is able to refer to "the pointer in which Foo will be stored" – but how does it do so? I kind of expect `init |out| Foo { ... }` where out is then a parameter that can be used with the address.
Benno: I agree with being able to customly name the variable, but having multiple at the same time might be a bit much... Although you are able to return multiple `impl Init` values, so it isn't too far fetched... hmmm
cramertj: Right, and functions regularly have multiple return values that they might wish to emplace.
cramertj: The other thing I was going to bring up is that if `-> Result<init T, E>` is used as sugar for `-> impl Init<T, E>`, we can't represent things that return values that aren't `Result` (e.g. `Option`, some other values unrelated to the initializer, or infallible cases-- modulo covering these with `-> impl Init<T, core::convert::Infallible`).
Josh: Given the precedent of `self` parameters being explicit, I think we'd want this to be explicit as well. (Whether or not we say that it has to be called `out`.)
If it's straightforward to parse and doesn't create ambiguities, I wonder if we could name return values *exactly* like we name arguments:
```rust
fn func() -> out: impl Init<SomeType> { ... }
```
tmandry: We can also use `@` here.
Benno: Replying to cramertj, I agree with your assesment of the `-> Result<init T, E>` sugar. We should only do that if `init T` can be placed anywhere in a type and the "correct" thing happens. I don't think that we can guarantee that, because what should happen for `Custom<init T>`?
## Const Support
Benno: I think we really ought to have const support in the language proposal. For `pin-init` this is a limitation that probably cannot easily be lifted, since closures (& traits) aren't supported in `const` contexts. Initializing a static using an initializer should be just as easy as initializing a stack local variable. This is also very important for e.g. global locks in the Linux kernel.
## Space-constrained future state? (Can be deferred if non-trivial)
Josh:
> So you might also use a solution that stores it on the stack if the future is small, and otherwise boxes it
This is inspiring me to wonder whether we could reasonably have a constraint on generic futures like "uses no more than X bytes of space for its state". This is something the compiler knows, and something the programmer might reasonably want to constrain. And this would be helpful for systems that want to do "store it on the stack" unconditionally, without the boxing fallback.
## Effects aside...
TC: It's neither here nor there, but in reading this...
> Avoiding stack overflows is the simplest use-case of in-place initialization, but it highlights one challenge with it. There is a tension between:
>
> 1. I want to write code that looks like `Box::new(MyLargeStruct::new())`, but
> 2. I want `Box::new` to run code *before* `MyLargeStruct::new()` runs any code.
>
> And in more complex cases such as `vec.push(MyStruct::new())`, you want `Vec::push` to run code both before *and* after the call to `MyStruct::new`.
...one can't help but note that inverting control flow in this manner is the problem effect handlers solve.
## `Zeroable` Support
Benno: In the `pin-init` crate (& thus in the Linux kernel), we have the `Zeroable` trait:
```rust
/// Marker trait for types that can be initialized by writing just zeroes.
///
/// # Safety
///
/// The bit pattern consisting of only zeroes is a valid bit pattern for this type.
pub unsafe trait Zeroable { /* functions omitted for brevity */ }
```
The `pin-init` crate has a derive macro for this trait that asserts that all
fields of a struct implement the trait. It supports writing
`..Zeroable::zeroed()` at the end of a struct initializer macro:
```rust
#[derive(Zeroable)]
struct MyStruct<'a> {
a: usize,
b: Option<&'a usize>,
c: [u8; 64],
}
fn foo(b: &usize) -> impl Init<MyStruct<'_>> {
init!(MyStruct {
b: Some(b),
..Zeroable::zeroed() // initializes all fields that aren't mentioned to zero
// ^^ only available because `MyStruct: Zeroable`.
})
}
```
We need this because the kernel has very big structs (tens of fields) that don't necessarily need to set all fields to a non-zero value (for example because they are vtables and store `Option<fn(...) -> ...>` where only a few functions are populated).
cramertj: This is also nice for working with OS APIs that hand out zero pages "for free", avoiding any initialization work.
cramertj: This feels similar to the APIs that are in the ZeroCopy crate (and project safe transmute). Is the suggestion to pull this into the language?
Benno: Just to stress that this is really important for the Linux kernel: if the language feature doesn't allow this (or extending the feature to support it), then RfL will probably need `pin-init` in parallel to the language feature :(
## `dyn Trait` not implementing `Trait`
Josh:
> Note that this usually implies that `dyn Trait` will *not* implement `Trait` when such mechanisms are used.
It seems like many potential solutions for `dyn Trait` on non-dyn-safe traits would like to lift this same requirement. And since the alternative would be not having `dyn Trait` for that trait at *all*, it seems potentially reasonable. What process should we follow for deciding that this is OK? And, should we label it somehow, such that you have to opt into having a `dyn Trait` for a `Trait` that isn't dyn-safe, by either using this solution, or by *omitting* methods that aren't dyn-safe?
Alice: Even if the return value really is `dyn Future` and not `impl Init<dyn Future>`, such as with Olivier's proposal that mirrors C++, does that help? The trait normally has a `Sized` bound on the return type in question after all ...
Benno: I remember a meeting some time back where we discussed this and we somehow worked around the issue (I'll try to dig it up, but might take a while)? I also remember suggesting that we just have to introduce a new notion of `dyn` that would be restricted.
Olivier: Having `dyn Trait` on non-dyn-safe traits would be useful for a great many things in the library I'm developing (Masonry). It's almost above variadic generics in my list of things I want from the language. Should probably become its own item/discussion.
tmandry: This is being implemented as part of the lang team experiment for async fn in dyn Trait. I think that will tell us the parameters of the design, and depending on how complex it is we could approve it as part of an RFC.
## Relationship to 2025H2 MIR Move Elimination
Yosh: The proposed [2025 H2: MIR Move Elimination](https://rust-lang.github.io/rust-project-goals/2025h2/mir-move-elimination.html) project goal seems very related to the work we're proposing here. From the proposal:
> Add a MIR optimization which eliminates move operations. This will require changes to the MIR semantics of move to enable the optimization to cases where a value has had its address taken (LLVM already eliminates moves when this is not the case).
Yosh: That seems to be the moral equivalent of C++'s [Deferred Temporary Materialization feature](https://devblogs.microsoft.com/cppblog/guaranteed-copy-elision-does-not-elide-copies/), but implemented at the compiler level rather than in the language. At the very least I wanted to point out that this seems closely related to the work we're proposing here, and we may want to think about ways in which both these efforts can integrate with one another.
Benno: I feel like explicit notation of creating in-place initializers & using them is paramount to their usefulness in situations where you *must not use the stack*. Solutions that "promise to optimize" are in my mind not confidence inspiring enough. Sure we can error after the optimizer determines that it's not possible, but that doesn't feel nice from an ergonomics point of view "why does my function not compile even though I feel like it should". Giving the user type-based feedback on why they are wrong seems much better IMO.
Yosh: oh yeah, absolutely - I agree. That's also why in my proposal I'm using an explicit `#[placing]` (placeholder) notation: optimizations are not enough; there must be a way to guarantee it as part of the contract.
Benno: :+1:
## No need for `#[placing]`
olivier: As far as I'm aware, the biggest difference between Yosh's proposal and the Placement-by-Return RFC is that Yosh's proposal adds a `#[placing]` annotation to functions that write their return with an outptr. I'm fairly sure this isn't needed in most cases.
Whether a function moves its returns or emplaces it is mostly invisible for the caller, except at an ABI level for pointer-sized types.
Benno: see my reply in the above discussion, sometimes you *need* in-placement to be guaranteed for a type or at a usage site, then explicitness is very much useful.
olivier: See mention of pinned places below. I think in 99% of cases, if you care about a type being emplaced, you want a pinned place. Also, I think something people usually miss is that you usually want *the caller* to give guarantees to the callee, not the other way around.
Benno: a reusable box doesn't necessarily pin it's contents, what if you want to temporarily store it in one and then move it into a `Vec` (& repopulate the box elsewhere again)?
olivier: True. To reformulate, my thought is: sometimes you'll want to annotate a local variable to say "this will never be moved, only emplaced at return". But you'll mostly never want to annotate a function with "whatever this returns wasn't moved, it was emplaced".
## Placement-by-return and pinned places
olivier: My thesis coming here would be: "We can get most of the benefits of Init/PinInit from placement-by-returns and pinned places, *except* for faillible placement, for which we strongly need Init/PinInit".
See https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/Limits.20of.20placement-by-return/near/522760324
for a discussion of what that would look like.
```rust
fn foobar() -> impl PinInit<PinnedStruct, Error> {
let pin mut s = pin_init!(
// Basically a closure
let pin a = pinned_value();
let b = result_value()?;
let pin c = try_pin_init!(pinned_result_value());
let pin s = PinnedStruct { a, b, c };
s
);
s.do_stuff();
s
}
fn pinned_value() -> pin A;
fn result_value() -> Result<B, Error>;
fn pinned_result_value() -> impl PinInit<C, Error>;
```
Yosh: The combination of "emplacement" and "fallibility" is definitely something that needs to be special-cased. But I don't believe that is inherently incompatible with the notion of "placing functions", annotated or otherwise. To elaborate, I think we agree that we want the following to work:
```rust
let x = foo()?; // `x` is always constructed in-place
```
cramertj: FWIW, this is not special-cased [in my outptr proposal](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#Uninitlto-Tgt-and-InPlacelto-Tgt)-- the returned value is inside a `Result<InPlace<'_, T>, E>`. I prefer approaches which allow nesting the in-place output inside another value.
Yosh: oh right - and that works because `InPlace<T>` doesn't initialize its values until a later use, right?
## How to evaluate and pick an approach
tmandry: The doc provides a nice survey of approaches, but I'm left wondering what direction we should go. What are the next steps to developing and evaluating approaches, and what input is needed from the lang team?
nikomatsakis: :+1: this was also my main question
Alice: My first question to the lang team is going to be what you think about the project goal proposal, and in particular the design axiom from there.
cramertj: I think it would be helpful to get a "vibe check" on the features that require native language support:
* `async fn` in `dyn Trait` returning an `impl Init`
* Adding new `Box` constructors which accept `impl Init` (also a T-libs question)
* [Gradual struct initialization](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#Ergonomics-extension-native-gradual-initialization) and, more generally, field projection for uninit
* [`&uninit` references](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#Composition-using-native-gradual-initialization) to create `Uninit<'_, T>` objects, projection to fields, and the ability to assign an `InPlace<'some_lt, T>` to a `T` that has been uninit-borrowed for lifetime `'_some_lt`
* [Syntax for in-place return values](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#Ergonomics-extension-in-place-return-values)
* `init` expressions
## (Answered) Why `*mut ()`?
Josh: Why does the `Init` trait use a raw pointer to *unit* (`*mut ()`) rather than a raw pointer to the *type*, or for that matter a `MaybeUninit` of the type?
Alice: The pointer needs to be thin when `T: !Sized`. The metadata is determined by `init` and the caller does not know
cramertj: `*const dyn Trait` isn't a thin pointer.
Josh: :+1:
In that case, could we please have a function `MaybeUninit::init` that takes an `impl Init` and *safely* initializes the type (such that you don't need any unsafe code calling `assume_init`)?
## Return value optimization & explicitness
Benno: above this example is given:
```rust
fn foo() -> MyStruct {
// Not last expression, so in-place init not used.
let x = MyStruct {
field: [0; 1024],
another_field: 20,
};
x
}
```
I think this is pretty bad from a design perspective, because the rules for what will enable RVO will most likely be non-trivial. Thus I prefer explicitly annotating in-place initialization (both usage site & declaration site).
Josh: :+1:, I agree. The solution this proposal seems to actually be making is explicit, so I think this proposal is aligned with that.
Olivier: To restate what I said earlier: I agree, but I think you want an annotation on `let x`, not on `fn foo()`.
## How should we weight use cases for returning types other than `T` and `Result<T, E>`, where `T` is initialized in-place?
tmandry: One advantage of cramertj's proposal is that it supports initializing into `Either<T, U>` where both `T` and `U` are initialized in-place. How much weight should the lang team place on this use case?
Alice: I think there's a big gap between supporting one out pointer and multiple out pointers.
## Comparing solutions: a table
cramertj:
| Proposal | Fallibility | `dyn` Compatible | Require explicit in-place initializations |
| - | - | - | - |
| [Alice's `init` exprs](https://hackmd.io/@aliceryhl/BJutRcPblx) | `impl Init<T, E>` / always `Result` | No | Yes |
|[placing functions](https://blog.yoshuawuyts.com/placing-functions/) | Yes | No | Yes |
| [cramertj's outptrs](https://hackmd.io/awB-GOYJRlua9Cuc0a3G-Q#In-place-initializion-via-outptrs) | `Result<InPlace<'_, E>` | Yes | Yes
| RVO | No | Yes | No
### Ergonomics
cramertj: All the proposals aside from my outptr proposal provide an ergonomic option for an initializer which returns a single value in-place. The outptr proposal would require an extra option in order to ergonomically initialize particular values.
### Things missing from the table
Benno: I am missing the following features in the table header:
* supports pinned initialization
* can work in `const` contexts
* `Zeroable` support as in `pin-init`
* uses explicit syntax for in-place initialization
cramertj; I think all of the proposals above support pinned initialization, work in `const` contexts, and could support `Zeroable`
Benno: AFAIK placing functions & RVO need pinned places.
cramertj: Right, there are other requirements in order to support `pin` well, but I think all of the options work.
## Way to materialization of nested `impl Init` in the main `Init` function
ding: Quick question. Suppose we take the route to the combined solution.
```rust=
fn combine(left: impl Init<A, !>, right: impl Init<B, !>) -> init (A, B) {
out.0 = <how to invoke in-place init left?>;
out.1 = <how to invoke in-place init right?>;
out
}
```
I am curious if we have a way to materialise the `left` and `right`, in the same sense that we materialise `Future`s with `.await`? Will this be considered in the design?
Olivier: The "Placement-by-return and pinned places" section has an example of how that might work with placement-return.
## Types with holes
Alice:
`Result<_, E>` -> `Result<Box<T>, E>` or `Result<&mut T, E>`
`Option<_>` -> `Option<Box<T>>` or `Option<&mut T>`
`_` -> `Box<T>` or `&mut T`
`(_, String)` -> `(Box<T>, String)`
## Vibe checks
tmandry: I'm partial to something that preserves execution order and composes arbitrarily well, which makes me favor the out pointer approach laid out by cramertj. My question is (1) whether we can make that thing maximally ergonomic (a la Yosh) and (2) how to handle AFIDT.
nikomatsakis: I think we should have an explicit syntax for the places where you need a guarantee and full generality, and maybe a "guaranteed optimization" for some cases that only require HIR analysis. Structs in trailing expr position only.
cramertj: That can get a little surprising, e.g. what happens if you do `Ok(match { .. => Struct })`
cramertj: My preference is that the places we guarantee are the same ones where we have explicit syntax.
alice: We could make that more convenient with some placement annotation that you put on a tail expr.
nikomatsakis: I like that, and we could error if we can't guarantee it.