Custom references
Custom references refers to a bundle of features that allow user-defined types to emulate and extend the capabilities of Rust references:
Resources
Field representing types (FRTs)
Note
This article contains parts of the current approach.
Field representing types, abbreviated FRTs, are a feature that allows writing code that is generic over the fields of structs, enums, tuples and variants of enums. They offer a limited form of reflection, as Rust code can inspect the fields of its own types.
Motivation
The most important application of FRTs is Field Projections. There they are one primitive way to construct Projections. FRTs can also be used by normal functions that need to be generic over fields, but do not fit into the field projection framework.
Naming FRTs
FRTs are named using the field_of! macro. They are available for structs,
unions, tuples and variants of enums:
#![allow(unused)]
fn main() {
use std::field::field_of;
struct MyStruct {
a: i32,
b: u32,
}
type A = field_of!(MyStruct, a);
type B = field_of!(MyStruct, b);
union MyUnion {
c: i32,
d: u32,
}
type C = field_of!(MyUnion, c);
type D = field_of!(MyUnion, d);
type E = field_of!((i32, u32), 0);
type F = field_of!((i32, u32), 1);
enum MyEnum {
Var1 { g: i32, h: u32 },
Var2(i32, u32),
}
type G = field_of!(MyEnum, Var1.g);
type H = field_of!(MyEnum, Var1.h);
type I = field_of!(MyEnum, Var2.0);
type J = field_of!(MyEnum, Var2.1);
}
An FRT is visible when the field it represents is visible. In particular, accessing the FRT of a private field from another module results in an error:
#![allow(unused)]
fn main() {
mod inner {
pub struct MyStruct {
a: i32,
pub b: i32,
}
}
type A = field_of!(inner::MyStruct, a); //~ ERROR: field `a` of struct `MyStruct` is private
type B = field_of!(inner::MyStruct, b);
}
The Field trait
FRTs implement the Field trait, which exposes information about the field
that they represent:
#![allow(unused)]
fn main() {
pub unsafe trait Field: Sized {
/// The type of the base where this field exists in.
type Base;
/// The type of the field.
type Type;
/// The offset of the field in bytes.
const OFFSET: usize;
}
}
Note that this trait cannot be implemented manually, so only FRTs implement it.
For example, considering the following type definitions from above:
#![allow(unused)]
fn main() {
struct MyStruct {
a: i32,
b: u32,
}
union MyUnion {
c: i32,
d: u32,
}
enum MyEnum {
Var1 { g: i32, h: u32 },
Var2(i32, u32),
}
}
We have the following:
field_of!(MyStruct, a)has:Base = MyStruct,Type = i32,OFFSET = offset_of!(MyStruct, a).
field_of!(MyUnion, c)has:Base = MyUnion,Type = i32,OFFSET = 0.
field_of!(MyEnum, Var1.g)has:Base = MyEnum,Type = i32,OFFSET = offset_of!(MyEnum, Var1.g).
Using FRTs
FRTs are usually used by APIs that wish to make an operation generically
available for each field of a struct, union, tuple or enum variant. To do so,
the API should introduce a generic parameter that implements the Field trait.
Since the trait cannot be implemented by non-FRTs, it ensures that only real
fields are allowed.
#![allow(unused)]
fn main() {
pub struct VolatileMut<'a, T: Copy> {
ptr: *mut T,
_phantom: PhantomData<&'a mut T>,
}
impl<'a, T: Copy> VolatileMut<'a, T> {
pub fn read_field<F: Field<Base = T>>(&self) -> F::Type {
let ptr = self.ptr.offset();
let ptr = unsafe { ptr.byte_add(F::OFFSET) };
let ptr = ptr.cast::<F::Type>();
unsafe { ptr.read_volatile() }
}
pub fn write_field<F: Field<Base = T>>(&mut self, value: F::Type) {
let ptr = self.ptr.offset();
let ptr = unsafe { ptr.byte_add(F::OFFSET) };
let ptr = ptr.cast::<F::Type>();
unsafe { ptr.write_volatile(value) };
}
}
}
Unresolved questions
- FRTs of structs, unions and tuples always exist in the type, but fields of enums are not necessarily accessible, as the value might not be of that variant. Probably need a separate trait to identify fields that always exist.
Projections
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Backlinks
HasPlace proposal
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
Truly First-Class Custom Smart Pointers | Nadri’s musings
Backlinks
Field projections
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
Project goal tracking issue, Fall 2025, contains many comprehensive updates
Design meeting 2025-08-13: Field projections - HackMD, Aug 2025
pre-RFC Field Projections v3 - HackMD, May 2025 or earlier
Backlinks
Place expression desugaring
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Backlinks
Reborrow
Reborrowing is an action performed on exclusive references which creates a copy of the source reference and marks it disabled for reads and writes. This retains the exclusivity of the reference despite creating a copy, as only one copy can be used at a time.
Today, true reborrowing is only available to Rust’s exclusive &mut T
references. Going beyond references means enabling true reborrowing for
user-defined exclusive reference types by defining a Reborrow trait.
We want to make it possible for both of the following functions to compile: an
exclusive &mut T reference and a user-defined custom exclusive reference
CustomMut<'_, u32> should have equivalent semantics.
Example:
#![allow(unused)]
fn main() {
fn f(a: &mut u32) {
f_x(a);
f_y(a);
}
fn g(a: CustomMut<'_, u32>) {
g_x(a);
g_y(a);
}
}
Use cases
Approaches
The current approach to reborrowing in user-land is based on an explicit method. The current work in the Reborrow traits lang experiment is based on a marker trait.
CoerceShared
Exclusive references call for a shared counterpart, into which an exclusive
reference can be coerced into. For Rust’s references, this is &T. For
user-defined exclusive reference types, a shared counterpart is a second
user-defined type that is freely shareable (read: is Copy). Coercing a
user-defined exclusive reference into a shared reference type requires defining
a CoerceShared trait.
Resources
Tracking Issue for Reborrow trait lang experiment · Issue #145612 · rust-lang/rust
Reborrow traits - Rust Project Goals, Jul 2025
rfcs/text/0000-autoreborrow-traits.md at autoreborrow-traits · aapoalas/rfcs, May 2025
Abusing reborrowing for fun, profit, and a safepoint garbage collector (conference talk with examples), Feb 2025
Backlinks
User-defined exclusive references
In some cases, users want to define their own custom reference types that have
equivalent semantics to Rust’s exclusive &mut T references but cannot, for
whatever reason, be directly expressible using them. For example, an exclusive
reference to unaligned data or an exclusive reference to a part of a matrix
could not be expressed using &mut T references. In other cases, the
exclusivity of the reference may not be a guarantee but more of a suggestion:
eg. for mutable C++ references it may be a good idea to try use them as
exclusive, but exclusivity is not guaranteed and thus using &mut T instead of
a custom type would cause undefined behaviour.
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct CustomMut<'a, T>(*mut T, PhantomData<&'a mut ()>);
}
Backlinks
Wrapper types
Currently Rust does not automatically reborrow Option<&mut T> or similar
wrapper types. Conceptually, there is no reason why &mut T should be
reborrowable but Option<&mut T> should not: the only difference between the
two types is that one can also be null.
With the Reborrow trait, reborrowing wrapper types of exclusive references becomes possible using blanket implementations.
#![allow(unused)]
fn main() {
impl<T: Reborrow> Reborrow for Option<T> { /* ... */ }
}
Backlinks
Marker types
Sometimes users want to define custom types with exclusive reference semantics that do not contain any pointers at all. This is useful in encoding exclusive access to data in the function’s indirect context. For example, embedded systems sometimes use reborrowable ZST marker types to pass exclusive access to hardware peripherals through their call stacks. The author uses a marker ZST to model garbage collection safepoints, ensuring that unrooted custom references to garbage collectable data in a system with a moving GC are not held past safepoints.
#![allow(unused)]
fn main() {
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
}
Backlinks
Exclusive reference collections
In some cases it can be useful to group up multiple exclusive references into a single collection.
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct MutCollection<'a, 'b, 'c> {
a: &'a mut A,
b: CustomMut<'b, B>,
c: Option<&'c mut C>,
}
}
Reborrowing such a collection as exclusive means simply reborrowing each exclusive reference individually and producing a new collection of the results. This can also be applied recursively:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct BiggerCollection<'a, 'b, 'c, 'd, 'e, 'f, 'g> {
one: MutCollection<'a, 'b, 'c>,
two: MutCollection<'d, 'e, 'f>,
three: CustomMut<'g, G>,
}
}
Backlinks
Marker trait approach
Note
This article contains parts of the current approach.
The current Reborrow traits lang experiment aims to produce a method-less
Reborrow trait. For exclusive reference semantics this is mostly trivial: the
concrete code generated for an exclusive reborrow is equivalent to that of
Copy, and only additional lifetime semantics must be considered on top.
This gives us a trivial, derivable Reborrow trait:
#![allow(unused)]
fn main() {
trait Reborrow {}
}
Its usage is performed through a derive macro:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
}
There are some limitations that we wish to impose on types that derive (or implement) this trait.
- The type must not be
CloneorCopy.
- Alternatively, blanket-implement
Reborrow for T: Copy, butClone + !Copytypes cannot beReborrow. - This limitation is placed to avoid types that are both
CopyandReborrow, as that only makes sense ifCopytypes are considered a “base case” for recursive reborrowing. Clone + !Copytypes generally only make sense if a correspondingDropimplementation exists, and reborrowing cannot be soundly combined withDrop. If a type isClone + !Copy + !Dropthen technically it can be soundly reborrowed, but it’s unlikely that the type itself makes overmuch sense (it is by definition a cloneable, ie. shareable, type with exclusive reference semantics).
- The type must not have a
Dropimplementation.
- Reborrowable types can have multiple owned copies in existence, and each one
of these would need to call
Drop(as we cannot know if we have the only copy or not). This is effectively guaranteed to end up causing a double-free.
- The type must have at least one lifetime.
- Alternatively, if
Copytypes blanket-implementReborrowthen this limitation cannot be placed, but manually derivingReborrowon a lifetime-less type should be an error. - This limitation is placed simply because a lifetime-less type cannot contain the lifetime information that sound reborrowing relies upon.
- The result of a
Reborrowoperation is simply the type itself, including the same lifetime.
- Reborrowing should produce a copy, not eg. a field or a new struct consisting of a subset of the type’s fields.
- If
Reborrowreturns a lifetime that is always shorter than the source lifetime, then values deriving from the operation cannot be returned past it up the call stack. A longer lifetime is of course meaningless. Thus, the same lifetime is what we should get.
Derivation of the Reborrow operation
Since the marker trait approach has no explicit fn reborrow method, the
compiler must derive the correct operation for Reborrow. When exactly one
lifetime exists on the type, that derivation is trivial: it is simply a Copy
and an exclusive reborrow of the singular lifetime. Though, this too can be
questionable:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Bad<'a>(&'a ());
}
Is the above type actually an exclusive reference? Is deriving Reborrow on it
an error? I think it should be, but it’s a little hard to exactly say why:
arguably it’s because reborrowing (re)asserts exclusive access while this type,
by definition, does not have exclusive access to anywhere. But maybe this is
created using a &mut () in which case it sort of does carry exclusive access,
it just cannot re-assert it.
If there are multiple lifetimes, then deriving Reborrow becomes more
complicated. Consider the following type:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Two<'a, 'b>(&'a (), &'b mut ());
}
This type should only exclusively reborrow the second reference.
These questions become a step more complicated once we give up on using Rust references and go into the world of custom types:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct MaybeBad<'a>(PhantomData<&'a ()>);
}
This type may or may not be bad: we simply have no idea. Whether the borrow
inside of PhantomData is &mut () or &() has no effect on whether or not
this type carries an “exclusive lifetime” or not.
This issue seems to call for a new kind of marker type:
#![allow(unused)]
fn main() {
/// Marker type for exclusive reference semantics.
#[derive(Reborrow)]
struct PhantomExclusive<'a>; // explicitly no content; this is purposefully bivariant.
impl<'a> PhantomExclusive<'a> {
/// Capture an exclusive reference into a PhantomExclusive.
fn from_mut<T>(_: &'a mut T) -> Self {
Self
}
/// Create a new unbound PhantomExclusive.
///
/// # Safety
///
/// * The caller must ensure that only one PhantomExclusive is created for
/// whatever that they're tracking.
unsafe new() -> Self {
Self
}
}
}
The compiler would track PhantomExclusive as an exclusive reference without
the pointer bits. Our custom type would then be:
#![allow(unused)]
fn main() {
#[derive(Reborrow)]
struct Good<'a>(PhantomData<&'a ()>, PhantomExclusive<'a>);
}
The first PhantomData is there to establish variance while the
PhantomExclusive is included to ensure 'a is an “exclusive lifetime”.
Recursive nature of deriving Reborrow
The derivation of Reborrow (and CoerceShared) has a recursive nature: we can
group up individual exclusive references (be they &mut T or CustomMut<'_, T>
or CustomMarker<'_>) into a single struct and derive Reborrow on it. This
derivation is done on the fields of the type, and when a field is found to be
Reborrow then that field’s reborrow operation becomes a part of the larger
whole’s operation.
This has some complexities regarding what are the “bottom/base cases” of the recursion.
&'a mut Tbottoms out and performs a reborrow on'aCustomMut<'a, T>can be assumed to bottom out and reborrow on'a.Two<'a, 'b>needs to be checked: does it reborrow both'aand'bor only one of them?PhantomData<&'a ()>bottoms out and DOES NOT perform a reborrow on'a: this is aCopymarker.PhantomExclusive<'a>bottoms out and performs a reborrow on'a.
Backlinks
Method-based approach
Warning
This article contains an older approach that has been abandoned.
Today, exclusive references can be implemented in user-land using a method-based approach:
#![allow(unused)]
fn main() {
trait Reborrow {
fn reborrow(&mut self) -> Self;
}
}
This captures the most important features of reborrowing: a source instance
self has exclusive access asserted on it, and a new Self is produced from it
(Some formalisations allow the method to choose its own result type using a
Self::Target ADT: this is arguably mixing up reborrowing with a generalised
Deref operation). However, this approach comes with downsides: the method
requires an explicit &mut self reference which bounds the resulting Self’s
lifetime to the calling function, and the method is user-overridable which leads
to arguably non-idiomatic “spooky action” and a possibility of misuse.
Bounded lifetimes
When the fn reborrow method is called in some outer function fn outer_fn,
The outer function must create a &mut T reference pointing to the value being
reborrowed:
#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: CustomMut<'a, u32>) -> &'a u32 {
// This:
inner_fn(t.reborrow());
// ... is equivalent to this:
let t_mut = &mut t;
inner_fn(t_mut.reborrow())
}
}
This means that the fn reborrow method is given a reference pointing to a
local value, effectively a pointer onto the stack. The compiler must make sure
that this pointer does not outlive the stack, which then means that the lifetime
of the resulting Self created by fn reborrow cannot outlive the function in
which it was created in. In the above example, this means that trying to return
the result of inner_fn will not compile because of the fn reborrow call,
citing “returns a value referencing data owned by the current function”.
Compare this to Rust references: the compiler understands that the result of
reborrowing a &mut produces a new reference that can be extended to the
original reference’s lifetime. This function compiles despite an explicit
reborrow being performed by the &mut *t code.
#![allow(unused)]
fn main() {
fn outer_fn<'a>(t: &'a mut u32) -> &'a u32 {
inner_fn(&mut *t)
}
}
We can make the code not compile by explicitly creating a &mut t reference:
#![allow(unused)]
fn main() {
fn outer_fn<'a>(mut t: &'a mut u32) -> &'a u32 {
// no longer compiles: returns a value referencing data owned by the current
// function
inner_fn(&mut t)
}
}
In user-land code bases that use the explicit fn reborrow method, there exists
a way to fix this issue: by simply removing the fn reborrow method call, the
original example code will compile. But knowing of this fix requires some deeper
understanding of the fn reborrow method and the borrow checker: it is not an
obvious or clean fix.
Most importantly, if the Rust compiler is in charge of automatically injecting
fn reborrow calls at appropriate use-sites, then it may not be feasible for
the compiler to perform the analysis to determine if a particular call should be
removed or not. Furthermore, in a post-Polonius borrow checker world it will
become possible for code like this to compile:
#![allow(unused)]
fn main() {
fn inner_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
// ...
}
fn outer_fn<'a>(t: &'a mut u32) -> Result<&'a u32, &'a u32> {
let result = inner_fn(t)?;
if result > 100 {
Ok(t)
} else {
Err(t)
}
}
}
This means that a reborrow of t must always happen, yet the lifetime expansion
of the result of inner_fn depends on whether the function returns Ok or
Err. In the Err branch the result’s lifetime must expand to that of the
source t, but in the Ok branch it must shrink to re-enable usage of the
source t. The method-based approach to Reborrow traits will not work here, as
the compiler cannot choose to call the method based on a result value that
depends on the call’s result.
One could argue that the compiler could simply extend the lifetime of the method call’s result, as it should only do good deeds. This may well open a soundness hole that allows safe Rust code to perform use-after-free with stack references.
User-controlled code
The second downside of the method-based approach is that reborrowing is fully invisible in the source code, and having user-controlled code appear where there is none visible is not very idiomatic Rust.
Consider eg. the following code:
struct Bad;
impl Reborrow for Bad {
fn reborrow(&mut self) -> Self {
println!("I'm bad!");
Self
}
}
fn main() {
let bad = Bad;
let bad = bad;
}
Depending on the exact implementation choices, this might print out the “I’m
bad” message. This is especially problematic if the compiler chooses to trust
that fn reborrow methods never store their &mut self reference given to them
and allows lifetime extension to happen:
#![allow(unused)]
fn main() {
struct Unsound<'a>(&'a mut &'a u32, u32);
// Note: lifetime for trait here to show that the compiler specially trusts that
// these lifetimes indeed are fully unifiable.
impl<'a> Reborrow<'a> for Unsound<'a> {
fn reborrow(&'a mut self) -> Self {
let data = &self.1;
self.0 = data;
Self(self.0, self.1)
}
}
}
This would absolutely not compile today, but if the compiler did truly trust that reborrow methods can do no wrong then something like this might just pass through the compiler’s intentional blindspot and become a soundness hole.
Backlinks
CoerceShared
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
If exclusive references exist, then shared references are nearly always
necessary as well. Rust’s own exclusive &mut T references automatically coerce
into shared &T references as necessary, and we want to enable this same
coercion for custom user-defined exclusive references as well. For this purpose
we define a CoerceShared trait.
Use cases
To be fleshed out. All the same cases apply as for Reborrow.
Note that some custom reference cases might carry extra metadata (eg. a usize)
for exclusive references which then gets dropped out when coercing to shared.
Approaches
The current approach to shared coercion in user-land is based on an explicit method. The current work in the Reborrow traits lang experiment is based on a marker trait.
Backlinks
Marker trait approach
Note
This article contains parts of the current approach.
Coercing a custom user-defined exclusive reference into a user-defined shared
reference is a slightly involved affair: the two types are obviously going to be
different types, just like &mut T and &T are different, but they must also
be similar enough that a pure marker trait can relate the types to one another.
From a user’s perspective, coercing an exclusive reference into shared is a simple operation: just name the source exclusive reference type and the target shared reference type.
This gives us the following trait definition:
#![allow(unused)]
fn main() {
trait CoerceShared<Target: Copy>: Reborrow {}
}
Its usage is done either manually through an impl Trait statement, or possibly
by deriving CoerceShared on the source exclusive reference type:
#![allow(unused)]
fn main() {
// impl Trait statement
#[derive(Reborrow)]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
struct CustomMarkerRef<'a>(PhantomData<&'a ()>);
impl<'a> CoerceShared<CustomMarkerRef<'a>> for CustomMarker<'a> {}
// derive macro
#[derive(Reborrow, CoerceShared(CustomMarkerRef))]
struct CustomMarker<'a>(PhantomData<&'a mut ()>);
struct CustomMarkerRef<'a>(PhantomData<&'a ()>);
}
As with the Reborrow marker trait, some limitations are placed on the trait although this time most of them are expressed on the trait directly.
- The type implementing
CoerceSharedmust also implementReborrow.
- Coercing an exclusive reference into a shared reference doesn’t make sense if the source type is not an exclusive reference.
- The result of the
CoerceSharedoperation must be aCopytype.
CoerceSharedcan be performed any number of times on the same value, always producing a byte-for-byte copy (ignoring any padding bytes). These results are thus copies of one another, so it must also be possible to performCoerceSharedonce and produce copies of that result.
- The result of the
CoerceSharedoperation must have at least one lifetime.
- A lifetime-less type cannot contain the lifetime information that sound reborrowing relies upon.
- The lifetime of the result must be equivalent to the source.
- If
CoerceSharedreturns a lifetime that is always shorter than the source lifetime, then values deriving from the operation cannot be returned past it up the call stack. A longer lifetime is of course meaningless. Thus, the same lifetime is what we should get.
- The target type must be relatable to the source type field-by-field.
- In order for the marker trait to be derivable by the compiler, its contents must be dericable from the source and target types. This is most reasonably performed field-by-field.
For exclusive reference types that have at most one data field and exactly one
lifetime, coercing into a shared reference type that has the same data field and
exactly one lifetime, the derivation of CoerceShared is trivial. For types
that have multiple fields and/or multiple lifetimes, the derivation becomes more
complicated.
Backlinks
Method-based approach
Warning
This article contains an older approach that has been abandoned.
Today, coercing exclusive references to shared references can be implemented in user-land using a method-based approach:
#![allow(unused)]
fn main() {
trait CoerceShared {
type Target: Copy;
fn coerce_shared(&self) -> Self::Target;
}
}
This approach suffers from the downsides as method-based Reborrow
does. In addition, it is not possible to fix the
lifetime issues by simply not calling the fn coerce_shared method as that
would mean trying to use a Self type where Self::Target is required.
The way to fix this is to define an Into<Self::Target> method that consumes
the source exclusive reference and produces a shared reference with the same
lifetime as a result. Then, instead of calling the fn coerce_shared method the
fn into method is called instead.
Associated type or type argument
In general, there is no reason for why an associated type would be preferable
versus a type argument in the CoerceShared trait: especially with exclusive
reference collections it might make sense for a single reborrowable type to have
multiple CoerceShared targets. If the compiler automatically injects the
correct fn coerce_shared method call, then an associated type becomes
preferable.
The problem is that if the requirements for implementing CoerceShared are not
strict enough and a type argument is used, then the trait could become a vehicle
for generic automatic value coercion. For example:
#![allow(unused)]
fn main() {
struct Int(i64);
impl CoerceShared<i64> for Int {
fn coerce_shared(&self) -> i64 {
self.0
}
}
impl CoerceShared<u64> for Int {
fn coerce_shared(&self) -> u64 {
self.0 as u64
}
}
impl CoerceShared<i32> for Int {
fn coerce_shared(&self) -> i32 {
self.0 as i32
}
}
impl CoerceShared<u32> for Int {
fn coerce_shared(&self) -> u32 {
self.0 as u32
}
}
// ... and so on ...
}
Backlinks
Receiver
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
The Receiver trait enables “arbitrary self types” by doing two things:
- Defining when a smart pointer type is allowed to be a method receiver.
- Generalizing method receivers past types that implement
Deref.
Resources
3519-arbitrary-self-types-v2 - The Rust RFC Book
- Tracking issue for RFC 3519:
arbitrary_self_types· Issue #44874 · rust-lang/rust - Arbitrary self types v2: stabilize by adetaylor · Pull Request #135881 · rust-lang/rust
Backlinks
Autoref
Note
This article contains parts of the current approach.
Autoref is the technical term used to describe the insertion of automatic borrowing of variables to calling methods. For example:
struct MyStruct;
impl MyStruct {
fn method(&self) {}
}
fn main() {
let my_struct: MyStruct = MyStruct;
my_struct.method();
// this works, because it is desugared into:
MyStruct::methods(&my_struct);
}
Going beyond references means adding support for autoref to custom types. The HasPlace proposal provides a way to borrow places, which we will explicitly use in this section. However, using that specific mechanism for borrowing is not required; autoref can work with other approaches as well.
Receiver::Target and HasPlace::Target
The HasPlace trait has an associated type called Target, which is the type of the place when dereferencing Self. A priori it is a different type from the associated type Target on the Receiver trait, which is responsible for allowing a type in the receiver position of a method. We have not yet settled the question on the relationship between the two Target types. The options are:
- Merge the
HasPlaceandReceivertraits. - Make
Receivera supertrait ofHasPlace. - Make
HasPlacea supertrait ofReceiver. - Keep them separate.
Options 1 and 2 are not a good idea, because implementing Receiver prevents a type from introducing inherent methods without breaking downstream users. For this reason we only consider options 3 and 4.
Option 3 could result in error messages that are confusing, since implementing HasPlace makes *p a valid expression (for p: Self ). However, any operation on *p (such as reading, writing, and borrowing) require additional traits to be implemented. If none are implemented, it could be strange to allow *p in the first place.
Option 4 has the disadvantage of making the model more complex; there are two Target types that one has to keep track of when a type implements both differently. Unless we discover a use-case for the diverging types, we will probably choose option 3.
A note on Deref
The discussions surrounding Receiver also mention Deref and there was a plan to add a supertrait relationship Deref: Receiver. HasPlace essentially supersedes Deref, which therefore takes it out of this question. We would like to make Deref: HasPlace, but that depends on the exact shape of HasPlace and the interaction with DerefMut.
Algorithm
An important idea behind this algorithm is that we make method resolution only dependent on Receiver and HasPlace. PlaceBorrow makes an appearance later, but does not drive method resolution. So we first compute the resolution algorithm and then check later if any place operations that we would need to perform are legal. If they aren’t, we error at that stage and do not go back to change the method we selected.
The algorithm gets invoked on all method calls. They are generally of the shape p.method() where p is a place expression. The method call can of course also have arguments, but they are ignored in the algorithm.
We first constructs a list of candidate types. This depends on whether the Target types of HasPlace and Receiver are unified or not.
- If they are unified, we compute the list
L := [T, T::Target, T::Target::Target, ...]. The computation of this list is described by the following code snippet:#![allow(unused)] fn main() { iter::successors(Some(T), |ty| { if ty.implements_has_place() { Some(ty.has_place_target()) } else { None } }) } - If they are separate, we compute the list
The computation of this list is described by this code snippet:L := flatten [ [ T, <T as Receiver>::Target, <<T as Receiver>::Target as Receiver>::Target, ... ], [ <T as HasPlace>::Target, <<T as HasPlace>::Target as Receiver>::Target, <<<T as HasPlace>::Target as Receiver>::Target as Receiver>::Target, ... ], [ <<T as HasPlace>::Target as HasPlace>::Target, <<<T as HasPlace>::Target as HasPlace>::Target as Receiver>::Target, <<<<T as HasPlace>::Target as HasPlace>::Target as Receiver>::Target as Receiver>::Target, ... ], ... ]#![allow(unused)] fn main() { iter::successors(Some(T), |ty| { if ty.implements_has_place() { Some(ty.has_place_target()) } else { None } }) .flat_map(|ty| iter::successors(Some(ty), |ty| { if ty.implements_receiver() { Some(ty.receiver_target()) } else { None } })) }
The second step in the algorithm is to iterate over the list of candidate types. Let U be the type that we are considering. We look through all impl blocks of the shape impl U and impl Trait for U (including generic ones such as impl<V> Trait for V where V can be substituted by U). This gives us a set of method candidates. If there is an inherent method, we pick that and continue with the next step. If there is a single trait method, we pick that. If there are multiple trait methods, we fail with an ambiguity error. If there are none, we proceed with the next element in the type candidate list.
The third step inspects the method, which has a general shape of fn method(self: X) again with function arguments omitted. Now we inspect X:
- If
Xoccurs in the candidate list that we walked to arrive at this method, we letq := *...*pbe suitably derefed to get toX, which is the number ofHasPlace::Targetwe go through. We then desugar the method toU::method(q)or<U as Trait>::method(q). - If
Xdoes not occur in the already considered candidates thenX: HasPlacemust be true. If that’s not the case, we emit an error.- If
X::Targetoccurs in the already considered candidate, we then letq := *...*pbe suitably derefed to get toX::Target. We then desugar toU::method(@X q)or<U as Trait>::method(@X q). - If
X::Targetdoes not occur in the list of already considered candidates, then we continue with the nextimplor type from the candidate list.
- If
Note that an alternative that we should consider is to error in the last case.
Note
The current algorithm for method resolution in Rust includes a final step where it applies array unsized coercions. See here for more information.
In this algorithm, we could add the same coercions at the end of each
HasPlacechain. An alternative would be to implementDereffor arrays with their target being the correct slice.
Examples
Direct call
#![allow(unused)]
fn main() {
impl Example { fn method(self: Arc<Self>); }
let example: Arc<Example>;
example.method();
// desugars to:
Example::method(example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: Arc<Self>)- found
X = Arc<Example>in candidate list at index 0 => no derefs are added and no borrow takes place
- found
- found inherent
Calling method twice will in this case result in an error, since Arc: !Copy. This is the same behavior as today. Reborrowing will also not change this for Arc, since that would require running custom code.
Basic reborrow
#![allow(unused)]
fn main() {
impl Example { fn method(self: ArcRef<Self>); }
let example: Arc<Example>;
example.method();
// desugars to:
Example::method(@ArcRef *example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: ArcRef<Self>)X = ArcRef<Example>not found in the candidate list, butX: HasPlaceX::Target == Examplefound at index 1 in candidate list,- => one deref is added and borrow using
ArcRef
- => one deref is added and borrow using
- found inherent
In this example, calling method twice will result in no error, since @ArcRef creates a new reference and increments the refcount.
No nested borrows
#![allow(unused)]
fn main() {
impl Example { fn method(self: &ArcRef<Self>); }
impl Trait for Example { fn method(self: &Self); }
let example: Arc<Example>;
example.method();
// desugars to:
<Example as Trait>::method(&*example);
}
Algorithm computation. Candidates: [Arc<Example>, Example]
Arc<Example>- no impl blocks contain a
fn method(self: X)
- no impl blocks contain a
Example- found inherent
fn method(self: &ArcRef<Self>)X = &ArcRef<Example>not found in the candidate list, butX: HasPlaceX::Target == ArcRef<Example>not found in candidate list- => continue with next impl/type
- found trait
fn method(self: &Self)inTraitX = &Examplenot found in the candidate list, butX: HasPlaceX:: Target == Examplefound at index 1 in candidate list,- => one deref is added and borrow using
&
- => one deref is added and borrow using
- found inherent
This example illustrates that we cannot “go through” multiple HasPlace::Target types and borrow them. This is because we only have an Arc and no ArcRef in memory where we could take a & of.
No “looking ahead” in the candidate list for borrowing
This example only works when Receiver and HasPlace can have divergent Target types.
#![allow(unused)]
fn main() {
struct Weird<A, B>(...);
impl<A, B> HasPlace for Weird<A, B> { type Target = A; }
impl<A, B> Receiver for Weird<A, B> { type Target = B; }
impl<A, B, P: Projection<Source = A>>
PlaceBorrow<P, Weird<P::Target, B>> for &A { ... }
impl &Example { fn method(self: Weird<Example, Self>); }
let example: &Example;
example.method();
//~^ ERROR: no method `method` found for `&Example`
}
Algorithm computation. Candidates: [&Example, Example]
&Example- found inherent
fn method(self: Weird<Example, Self>)X = Weird<Example, &Example>not found in candidate list, butX: HasPlaceX::Target == Examplenot found in candidate list (we only check up to the point where we currently are at!)- => continue with next impl/type
- found inherent
Example- no impl block contains a
fn method(self: X)
- no impl block contains a
- Error, since the end of the list is reached.
Place wrapper
#![allow(unused)]
fn main() {
impl Example { fn method(self: Pin<&mut MaybeUninit<Self>>); }
struct Parent { example: Example }
let parent: Pin<Box<MaybeUninit<Parent>>>;
parent.example.method();
// desugars to:
Example::method(&pin mut (@%MaybeUninit (**parent).example));
}
Note
The place expression
parent.exampleis desugared to@%MaybeUninit (**parent).example, which has the typeMaybeUninit<Example>, see place expression desugaring. Place expressions are passed to the method resolution algorithm in their desugared form.
Algorithm computation. Candidates: [MaybeUninit<Example>, Example]
MaybeUninit<Example>- no impl block contains a
fn method(self: X)
- no impl block contains a
Example- found inherent
fn method(self: Pin<&mut MaybeUninit<Self>>)X = Pin<&mut MaybeUninit<Example>>not found in candidate list, butX: HasPlaceX::Target == MaybeUninit<Example>found in candidate list at index 0- => no derefs are added and borrow using
Pin<&mut MaybeUninit<Example>>
- => no derefs are added and borrow using
- found inherent
Deep deref
#![allow(unused)]
fn main() {
impl Example { fn method(self: &Self); }
let example: Box<Box<Box<Box<Example>>>>;
example.method();
// desugars to:
Example::method(&****example);
}
Algorithm computation. Candidates: [Box<Box<Box<Box<Example>>>>, Box<Box<Box<Example>>>, Box<Box<Example>>, Box<Example>, Example]
Box<Box<Box<Box<Example>>>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Box<Box<Example>>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Box<Example>>- no impl block contains a
fn method(self: X)
- no impl block contains a
Box<Example>- no impl block contains a
fn method(self: X)
- no impl block contains a
Example- found inherent
fn method(self: &Self)X = &Examplenot found in candidate list, butX: HasPlaceX::Target == Examplefound in candidate list at index 4- => 4 derefs are added and borrow using
&
- => 4 derefs are added and borrow using
- found inherent
Resources
- Autoref and next gen place traits | Meeting by Xiang, Nadri and Benno, Jan 2026
- Autoref and Autoderef for First-Class Smart Pointers | Nadri’s musings, Dec 2025
- Ensure
arbitrary_self_typesmethod resolution is forward-compatible with custom autoref · Issue #136987, Feb 2025- issue comment with design sketch (see “Summary sketch”)
Backlinks
In-place initialization
Initializing values in-place without copying them. This eliminates unnecessary copies and allows for self-referential datastructures.
Range of use cases
TODO: Cover the range of use cases like
- Pinned vs unpinned
- Constructor functions
- Fallible
Approaches
Potential design axioms
Here we propose design axioms that proposals should be evaluated against. Some criteria in this list might be contradicting each other or mutually exclusive. Nevertheless they are used to understand the design space and measure trade-offs of various solutions.
The list is presented not in the order of priority.
-
Work with fallibility in mind.
- A successful proposal should demonstrate that during the initialisation journey, how errors can be handled and initialisation process could be interrupted gracefully.
- A litmus test is a demonstration of an emplacing function that returns a
Resultor anOption.
-
Minimal surprise - drop semantics
-
There are implicit structure in Rust program semantics such as the concept of RAII.
-
When applicable, a successful proposal should demonstrate that a novice user could use intuition developed from the rest of the Rust language identify locations in a user code with the following program semantics, with little help from language servers or regular expression that is specially engineered with effort.
- The location where the initialisation is complete.
- The location where the variable is considered initialised.
- The location where the drop obligation of the values is activated.
-
A litmus test should be a test on the following code templated with the respective proposals.
#![allow(unused)] fn main() { // This notation is only to introduce a goal for emplacement. // Respective proposals should fill in their code with their proposed syntax. let x: BigStructWithThousandFields; // Goal 1. Initialise `field1`, whose type has a destructor <expr>; if random() { panic!() } // Snap quiz to the tester: what is the state of `field1` at this location? // Goal 2. Re-initialise `field1` <expr>; // Snap quiz to the tester: what is the state of `field1` at this location? // Goal 3. Initialise the other 999 `field<n>`s; abbreviation is encouraged. // Note that the expressions may be interleaved with `do_something()` // **and unpredictable re-initialisation of fields**. <exprs>; do_something(); <exprs, possibly re-initialisations>; // Snap quiz to the tester: what is the state of `x` at this location? Does `x` get dropped // when do_something panics? do_something(); // Snap quiz to the tester: what is the state of `x` at this location? Does `x` get dropped // when do_something panics? // Goal 4. When applicable, finalise the emplacement. <finalisation>; // Snap quiz to the tester: what is the state of `x` at this location? }
-
-
No or minimal
panicin the generated code.- A solution should be used even in no-panic platforms.
- A succesful proposal should demonstrate that no panics can arise from an emplacement process, or
how a failure could be externalised.
Also refer to the criterion
Work with fallibility in mind.
-
Panic safety.
- A successful proposal should demonstrate that it admits panic safety- destructors are properly called and all live references remain valid on every possible unwind paths.
- A litmus test would be, taking emplacement example code under a proposal, insert an explicit panic at each possible program point and check that the panic safety is upheld at this panic point.
-
Support abstraction across function boundaries.
- A successful proposal should demonstrate how the information of state of emplacement can propagate across function calls, which is crucial to write subroutines and coroutines that collectively complete an in-place initialisation.
-
Composability of emplacing functions.
- A successful proposal should demonstrate how one writes emplacing function combinators so that
they can be used together to complete the entire emplacement operation.
As an example, the in-place initialiser of a container like
Mutex<_>can be viewed as a combinator that turns an emplacing initialiser of a typeMyStructinto an emplacing initialiser of a typeMutex<MyStruct>.
- A successful proposal should demonstrate how one writes emplacing function combinators so that
they can be used together to complete the entire emplacement operation.
As an example, the in-place initialiser of a container like
-
Good composibility with any control-flow.
- This covers control-flow constructs such as early returns via the try
?operator, coroutine suspensionyieldandasyncfuture suspensionawaitas well as thebreak,continuekeywords usable in loops
- This covers control-flow constructs such as early returns via the try
-
No or minimal
unsafe.- A successful proposal should demonstrate that the compiler can apply analysis to the maximal
degree and provide maximal safety without resorting to requiring user assertions by marking
code
unsafe.
- A successful proposal should demonstrate that the compiler can apply analysis to the maximal
degree and provide maximal safety without resorting to requiring user assertions by marking
code
-
Minimal changes to smart pointer and container APIs for emplacement.
- Ideally we have seamless support by
BoxandVecas a starting point. - A successful proposal should answer satisfiably how standard library containers can support emplacement, in best cases extending existing functions without breaking changes.
- Ideally we have seamless support by
-
Explicit syntatical signal to emplace values in-place
- When the emplacement intention is signaled through syntax, the emplacement must take place with a given memory slot.
- A successful proposal should guarantee compile error if emplacement is impossible or illegal.
-
Support for blanket initialisation traits
- A litmus test is a version of
Zeroabletrait under the respective proposals. This is a trait that is implemented on types with all-zeros as a valid bit-pattern. - A successful proposal should allow
Zeroabletrait to signal a specific way to fallback during emplacement, in case astructfield is left unspecified. Inpin-initcrate syntax the trait is used at a symbolic..struct base expression, so that the unmentioned fields are filled with zeros through theZeroable::init_zeroed()method.
- A litmus test is a version of
-
Composability with various function modifiers or flavours
- It is also loosely connected the the notion of “effects” in the following themes.
- Fallibility, functions whose return types implements
Trysuch asResult,OptionandPoll constand in association the host effect under the theme of constant-time evaluationasync,async genandgenunder the theme of coroutinesPined-ness in the theme of self-referential data- … and combination of all function flavours above
- Fallibility, functions whose return types implements
- A successful proposal should demonstrate good synergy with the mentioned “effects” or at least prospect of future extensions to support “effects” and “modifications.”
- It is also loosely connected the the notion of “effects” in the following themes.
-
Backward-compatibility
- Existing functions in traits definitions are automatically emplacing without any syntactical rewrites.
- A succesful proposal should allow, among all traits,
impl Froms to be automatically emplacing without any SemVer breaking change that warrants a major version bump across the Rust ecosystem.
Resources
In-place initialization - Rust Project Goals, Fall 2025
#t-lang/in-place-init > in-place initialization: RfL design wishes - rust-lang - Zulip
Init expression and Init traits
Summary
We are inspired by the already successful pin-init trait and lift the initialising closure macros into a language syntax.
For this, our proposal will introduce Init traits that powers the initialisaing closures, as well as the syntax to allow users to write such closures conveniently.
The main proposal
For the main points, we will direct you to the original proposal here.
Addendum
To address some concerns and introduce enhancements, we will direct you to the addendum content here.
Backlinks
Summary
This is the document archived from Init expressions / In-place initialization authored by Alice Ryhl.
Introduce a new trait Init for initializers. Initializers are essentially
functions that write a value to the place provided to it. Along with the trait,
we also introduce:
- A second trait
PinInitfor handling the case where the value is pinned after initialization. - A new syntax
init {expr}that creates an initializer. This lets you compose initializers, and it is essentially a type of closure. - A new mechanism so that dyn compatible traits can have methods returning
impl Trait(including async fns).
Motivation
This document outlines a mechanism for in-place initialization. I believe it solves the following issues:
- Stack overflow when creating values on the heap
- Constructors returning pinned values
- C++ move constructors
- Async fn in dyn Trait
It works both with values that require pinning, and values that do not.
Stack overflow when creating values on the heap
If you attempt to allocate a box with Box::new(value), then the compiler will
often generate code that performs the following steps:
- Construct
valueon the stack. - Allocate memory on the heap.
- Move
valuefrom the stack to the heap.
If value is large, then this may result in a stack overflow during the first
step. The stack overflow can be avoided if the compiler instead performed these
steps:
- Allocate memory on the heap.
- Construct
valuedirectly on the heap.
However, the compiler is often unable to perform this optimization because the
straight-line execution of Box::new(value) is that value should be created
before allocating memory.
In-place initialization provides a convenient alternative to Box::new(value)
where the straight-line execution is to first allocate the memory and then
construct the value directly on the heap. This means that the behavior we want
does not require any optimization.
Constructors returning pinned values
You might want to implement a linked list like this:
#![allow(unused)]
fn main() {
struct Entry {
next: *mut Entry,
prev: *mut Entry,
}
struct List {
// The list has an extra entry `dummy` that does not correspond to an actual
// element. Here, `dummy.next` is the first actual element and `dummy.prev`
// is the last actual element.
dummy: Entry,
}
impl List {
fn new() -> List {
List {
dummy: Entry {
next: < what goes here? >,
prev: < what goes here? >,
}
}
}
fn push_front(&mut self, entry: *mut Entry) {
entry.next = self.dummy.next;
entry.prev = self.dummy;
self.dummy.next = entry;
}
}
}
Here, List::new() needs to initialize the pointers to dummy so that
push_front has the correct behavior when pushing to an empty list. But that is
tricky to do. This implementation does not work:
#![allow(unused)]
fn main() {
impl List {
pub fn new() -> List {
let mut me = List { ... };
me.dummy.prev = &mut self.dummy;
me.dummy.next = &mut self.dummy;
// Oops, this changes the address of `me`.
me
}
}
}
In-place initialization makes it possible to write constructors like this, as the final address is known during initialization when using in-place initialization.
This implementation of linked list is extremely common in the Linux kernel
because it avoids a branch for the empty list case in push. Linux uses it
inside most data structures. For example, the Linux kernel mutex uses this type
of linked list for the queue of waiters.
C++ move constructors
Most values in C++ are not trivially relocatable. This means that you must run user-defined code (the move constructor) to move them from one address to another. It would be nice if we could move a C++ value using code along these lines:
#![allow(unused)]
fn main() {
let new_value = old_value.take();
}
where take() creates an initializer that calls the C++ move constructor.
In-place initialization makes code like this possible because it lets
take() know what the address of new_value is.
Calling the move constructor still requires an explicit call to take(), but
Rust generally prefers for such operations to be explicit anyway, so I don’t see
this as a downside.
Async fn in dyn Trait
Given the following trait:
#![allow(unused)]
fn main() {
trait MyTrait {
async fn foo(&self, arg1: i32, arg2: &str);
}
}
we would like MyTrait to be dyn compatible. It is not dyn compatible today
because <dyn MyTrait>::foo returns a future of unknown size, and the caller
needs to know the size.
There are various proposal for how to solve this, the most prominent being
autoboxing where the compiler generates a <dyn MyTrait>::foo that returns
Pin<Box<dyn Future>>. Boxing the future solves the issue with the size being
unknown, but autoboxing has several downsides:
- The allocation strategy is not configurable. I.e., you could not implement your own logic that conditionally stores the future on the stack or the heap depending on the size.
- It might require new syntax such as
my_trait.foo().box. - It introduces implicit calls to
Box::newin the language itself, which many community members including myself believe to be against the design goals of Rust.
In-place initialization provides a solution that lets you call an async function
<dyn MyTrait>::foo without any of the above downsides.
Note that it generalizes to
#![allow(unused)]
fn main() {
trait MyTrait {
fn foo(&self, arg1: i32, arg2: String) -> impl MyOtherTrait;
}
}
as long as MyOtherTrait is dyn-compatible.
Guide-level explanation
The standard library has two traits that looks like this:
#![allow(unused)]
fn main() {
/// Initializers that can create values using in-place initialization.
unsafe trait PinInit<T> {
type Error;
unsafe fn init(self, slot: *mut T) -> Result<(), Self::Error>;
}
/// Initializers that can create values using in-place initialization.
///
/// Note that all initializers also implement `PinInit`, since you can always
/// pin a value immediately after creating it.
trait Init<T>: PinInit<T> { }
}
The real traits are slightly more complex to support T: ?Sized, but are
otherwise equivalent to the above. See the reference-level explanation for the
full traits.
The standard library also comes with the following blanket implementations:
#![allow(unused)]
fn main() {
impl<T> PinInit<T> for T {
type Error = Infallible;
fn init(self, slot: *mut T) -> Result<(), Infallible> {
slot.write(self);
Ok(())
}
}
impl<T,E> PinInit<T> for Result<T,E> {
type Error = E;
fn init(self, slot: *mut T) -> Result<(), E> {
slot.write(self?);
Ok(())
}
}
impl<T> Init<T> for T {}
impl<T,E> Init<T> for Result<T,E> {}
}
Using initializers
Using the above declarations, many constructors become possible. For example, we
may write these for Box.
#![allow(unused)]
fn main() {
impl<T> Box<T> {
pub fn try_init<E>(i: impl Init<T, Error = E>) -> Result<Box<T>, E> {
let mut me = Box::<T>::new_uninit();
i.init(me.as_mut_ptr())?;
Ok(me.assume_init())
}
pub fn init(i: impl Init<T, Error = Infallible>) -> Box<T> {
Box::try_init(i).unwrap()
}
pub fn try_pinit<E>(i: impl PinInit<T, Error = E>) -> Result<Pin<Box<T>>, E> {
let mut me = Box::<T>::new_uninit();
i.init(me.as_mut_ptr())?;
Ok(Pin::from(me.assume_init()))
}
pub fn pinit(i: impl Init<T, Error = Infallible>) -> Pin<Box<T>> {
Box::try_pinit(i).unwrap()
}
}
}
Other possible cases are constructors for Arc, or methods such as
Vec::push_emplace.
Initializers may also be used on the stack with the macros init! or pinit!.
#![allow(unused)]
fn main() {
let value = init!(my_initializer);
let value2 = pinit!(my_pin_initializer);
}
Note that pinit! internally works like the pin! macro and returns a
Pin<&mut T>. The init! macro returns a T.
The init syntax
A new syntax similar to closures is introduced. It allows you to compose
initializers. The syntax is init followed by an expression. As an example,
#![allow(unused)]
fn main() {
init MyStruct {
field_1: i1,
field_2: i2,
}
}
desugars to an initializer that creates a MyStruct by first running i1 to
create field_1 and then running i2 to create field_2. Arrays are also
supported:
#![allow(unused)]
fn main() {
init [i; 1_000_000]
}
desugars to an initializer that evaluates i one million times.
This logic does not work recursively. To get recursive treatment, you need
to use init multiple times.
#![allow(unused)]
fn main() {
init MyStruct {
foo: init (17, init MyTupleStruct(i1)),
bar: init MySecondStruct {
baz: init [i2; 10],
raw_value: my_value,
}
}
}
This ultimately treats 17, i1, i2, and my_value as the initializers to
run. Note that due to the blanket impls that makes any type an initializer for
itself, using 17 and my_value works seamlessly even if they’re just the
value to write.
All initializers in an init expression must have the same error type. If initialization of a field fails, then the previously initialized fields are dropped and the initialization of the struct fails.
The initializers are run in the order they appear in the init expression.
Because of that, previous fields may be accessed by name in the expression for
creating the next initializer. For example, you can create a field that holds
the same value twice like this:
#![allow(unused)]
fn main() {
init MyStruct {
foo: my_expensive_initializer(),
bar: foo.clone(),
}
}
You may also use an underscore to run additional code during the initializer:
#![allow(unused)]
fn main() {
init MyStruct {
foo: 12,
_: {
println!("Initialized foo to {foo}.");
},
}
}
The RHS of an underscore must evaluate to an initializer for (). They may be used to modify previous fields, or to fail the initializer by returning an error.
#![allow(unused)]
fn main() {
struct MyStruct {
inner: bindgen::some_c_struct,
}
impl MyStruct {
fn new(name: &str) -> impl Init<MyStruct, Error> {
init MyStruct {
inner: unsafe { core::init::zeroed() },
_: {
let ret = unsafe {
bindgen::init_some_c_string(&mut inner, name)
};
if ret < 0 {
Err(Error::from_errno(ret))
} else {
// Result<(), Error> is an initializer
// for ().
Ok(())
}
},
}
}
}
}
Pinned fields
Normally, all initializers using in an init expression must implement the
Init trait. However, this can be relaxed to only requiring PinInit using the
#[pin] annotation.
#![allow(unused)]
fn main() {
struct MyStruct {
f1: String,
#[pin]
f2: MyPinnedType,
}
}
In this case, init MyStruct { f1: i1, f2: i2 } requires i1 to implement
Init<String>, but the requirements for i2 are relaxed to only require
PinInit<MyPinnedType>.
The opaque type returned by an init expression always implements PinInit. It
implements Init if and only if it is composed using only initializers that
implement Init.
Whenever #[pin] is present on at least one field, implementations of Drop
need to use the signature fn drop(self: Pin<&mut Self>) instead of the normal
signature. The compiler additionally allow you to obtain an Pin<&mut Field>
given an Pin<&mut MyStruct> when a field is annotated with #[pin].
Similarly, you can obtain &mut Field given Pin<&mut MyStruct> when a field
is not annotated with #[pin]. The compiler-generated impl for Unpin needs to
be adjusted to match.
Or in other words, we make pin-project into a language feature over an
edition.
Impl trait in dyn trait
If you have a trait such as
#![allow(unused)]
fn main() {
trait MyTrait {
fn foo(&self, arg1: i32, arg2: &str) -> impl MyOtherTrait;
}
}
then MyTrait is dyn-compatible with <dyn MyTrait>::foo being a
compiler-generated method that returns an opaque type that implements Init<dyn MyOtherTrait>.
This allows you to do things such as:
#![allow(unused)]
fn main() {
trait MyTrait {
async fn call(&self);
}
async fn my_fn(value: &dyn MyTrait) {
Box::pinit(value.call()).await;
}
}
This is nice since it is explicit that you’re boxing the future returned by
call so that you can await it.
Of course, value.call().await would not work as you don’t have a future.
However, we should be able to emit good error messages for this case suggesting
that you wrap it in a box.
One advantage of this design is the flexibility with regards to boxing. The above example shows that we can box the future, but you could also easily support storing it on the stack if the future is small, and only allocate a box for large futures.
Note that dyn MyTrait does not implement MyTrait under this design, as <dyn MyTrait>::call does not return a future.
Reference-level explanation
We add the following two traits to core::init.
#![allow(unused)]
fn main() {
/// # Safety
///
/// Implementers must ensure that if `init` returns `Ok(metadata)`, then
/// `core::ptr::from_raw_parts_mut(slot, metadata)` must reference a valid
/// value owned by the caller. Furthermore, the layout returned by using
/// `size_of` and `align_of` on this pointer must match what `Self::layout()`
/// returns exactly.
unsafe trait PinInit<T: ?Sized + Pointee> {
type Error;
/// Writes a valid value of type `T` to `slot` or fails.
///
/// If this call returns `Ok`, then `slot` is guaranteed to contain a valid
/// value of type `T`. If `T` is unsized, then `slot` may be combined with
/// the metadata to obtain a valid pointer to the value.
///
/// Note that `slot` should be thought of as a `*mut T`. A unit type is used
/// so that the pointer is thin even if `T` is unsized.
///
/// # Safety
///
/// The caller must provide a pointer that references a location that `init`
/// may write to, and the location must have at least the size and alignment
/// specified by `PinInit::layout`.
///
/// If this call returns `Ok` and the initializer does not implement
/// `Init<T>`, then `slot` contains a pinned value, and the caller must
/// respect the usual pinning requirements for `slot`.
unsafe fn init(self, slot: *mut ()) -> Result<T::Metadata, Self::Error>;
/// The layout needed by this initializer.
fn layout(&self) -> Layout;
}
/// Indicates that values created by this initializer do not need to be pinned.
///
/// # Safety
///
/// Implementers must ensure that the implementation of `init()` does not rely
/// on the value being pinned.
unsafe trait Init<T: ?Sized + Pointee>: PinInit<T> {}
}
The standard library also comes with the following implementations:
#![allow(unused)]
fn main() {
unsafe impl<T> PinInit<T> for T {
type Error = Infallible;
fn init(self, slot: *mut T) -> Result<(), Infallible> {
slot.write(self);
Ok(())
}
fn layout(&self) -> Layout {
Layout::new::<T>()
}
}
// SAFETY: Even if `T: !Unpin`, this impl can only be used if we have `T` by
// ownership which implies that the value has not yet been pinned.
unsafe impl<T> Init<T> for T {}
impl<T,E> PinInit<T> for Result<T,E> {
type Error = E;
fn init(self, slot: *mut ()) -> Result<(), E> {
slot.write(self?);
Ok(())
}
fn layout(&self) -> Layout {
Layout::new::<T>()
}
}
impl<T,E> Init<T> for Result<T,E> {}
}
Creating initializers
The init expression is parsed into one of the following cases:
Struct syntax
If it matches init StructName { ... }, then the struct expression is parsed to
obtain an ordered list of fields. Fields can take the following forms:
field: rhsname @ field: rhs_: rhs
This results in an initializer whose init function runs the initializers in
the order they are listed. That is, to construct a field, it will call
PinInit::init(rhs, <ptr to field>).
When evaluating rhs, previously initialized fields are in scope. The
name @ field syntax may be used to rename what a field is called in subsequent
initializers.
When the field name is an underscore, it is treated like a field of type ().
Unlike initializers for named fields, there may be multiple such underscore
initializers in a single struct initializer. Underscore fields cannot be named
with @, but they may use super let to define variables that are accessible
in later initializers.
Note that the initializers for fields (but not underscores) are required to
implement Init in addition to PinInit unless the field is annotated with
#[pin] in the declaration of the struct.
If the initializer of any field fails, then the entire initializer fails.
Previously initialized fields (and super let variables) are dropped in reverse
declaration order, using the order they are declared in the init expression.
Tuples and array syntax
If it matches init (i1, ..., in) or init [i1, ..., in], then you get an
initializer whose init function runs the initializers in order to construct a
tuple or array. That is, for each k it calls PinInit::init(ik, <ptr to kth slot>).
There is no syntax for initializing tuples or arrays out of order, or for accessing previously initialized values.
Tuples and arrays never require the initializers to implement Init. That is,
it behaves as-if the “fields” of the tuple or array are annotated with #[pin].
This implies a decision that tuples and arrays always structurally pin their
contents, which Rust hasn’t yet made a decision on, but structural pinning
is the natural default.
Tuple structs
If it matches init StructName(i1, ..., in), then you get an initializer whose
init function runs the initializers in order. That is, for each k it calls
PinInit::init(ik, <ptr to kth field>).
Tuple structs are treated exactly the same as tuples, except that it follows the
same rules as structs with regards to #[pin] annotations on fields.
It’s not possible to access previous fields using this syntax. It’s also not possible to reorder the fields. To do that, you may use the full struct syntax instead:
#![allow(unused)]
fn main() {
init StructName {
0: init_field_0(),
2 @ foo: init_field_2(),
1: init_field_1(&foo),
}
}
Enums and unions
The syntax for structs and tuple structs also works with unions and enums. For
example, if you write init MyEnum::MyCase { field: initer }, then that will
initialize an enum. Same applies to init Ok(initer) to initialize a Result.
Note that enums are not syntactically different from the struct or tuple
struct cases, since init MyEnum::MyCase could syntactically just as well be
a struct inside a module.
The same applies to unions. In this case, the struct syntax should mention exactly one of the union’s fields.
Arrays with repetition
If it matches init [i; N], then the initializer evaluates i repeatedly N
times.
Note that repetition does not clone the initializer. Rather, it evaluates the expression many times, similar to the body of a for loop.
Blocks
If it matches init { ... } then the initializer just evaluates the block. The
last expression of the block must be another initializer.
This case may be used to define variables used by other parts of the initializer. For example:
#![allow(unused)]
fn main() {
init {
let value = expensive_logic();
init [value; 1000]
}
}
creates an initializer that evaluates expensive_logic() once and copies the
output 1000 times, as opposed to init [expensive_logic(); 1000] that calls
expensive_logic() multiple times.
When does init implement Init?
The opaque type of an init expression always implements PinInit, but only
implements Init if all initializers used to construct it implement Init.
Capturing semantics
Any locals used inside an init expression are captured using move semantics
similar to move || { ... } closures.
Pin annotations
Over an edition boundary, we introduce a new annotation #[pin] that may be
used on struct fields:
#![allow(unused)]
fn main() {
struct Foo {
#[pin]
pinned_field: F1,
not_pinned_field: F2,
}
}
This annotation affects whether init expressions require initializers for the
fields to implement Init, but it also has several other effects:
Destructors
When a field has a #[pin] annotation, you must use a different signature to
implement Drop.
#![allow(unused)]
fn main() {
impl Drop for Foo {
fn drop(self: Pin<&mut Foo>) {
}
}
}
Unpin impl
The Unpin impl automatically generated by the compiler is modified so that it
only has where clauses for fields with the #[pin] annotation.
#![allow(unused)]
fn main() {
// compiler generated
impl Unpin for Foo
where
F1: Unpin,
{}
}
If the Unpin trait is implemented manually, then it is an error to use the
#[pin] annotation on any fields of the struct.
Note that this requires that we change the default compiler generated
implementation of Unpin. This is possible over an edition boundary.
Projections
Given a Pin<&mut Struct> to a Struct that is defined in the new edition and
doesn’t have a manual Unpin implementation, you may project Pin<&mut Struct>
to either Pin<&mut Field> or &mut Field depending on whether the field is
annotated with #[pin] or not.
Impl trait in dyn trait
Using impl Trait in return position no longer disqualifies a trait from being
dyn compatible. Specifically, the trait is dyn compatible if replacing impl Trait with dyn Trait in the return type results in a valid unsized type.
Some examples:
#![allow(unused)]
fn main() {
struct Helper<T, U: ?Sized> {
foo: T,
bar: U,
}
// OK, return type is `dyn MyOtherTrait`.
trait Trait1 {
fn foo(&self) -> impl MyOtherTrait;
}
// OK, return type is `Helper<String, dyn MyOtherTrait>`.
trait Trait2 {
fn foo(&self) -> Helper<String, impl MyOtherTrait>;
}
// BAD, `Helper<dyn MyOtherTrait, String>` is an invalid type.
trait Trait3 {
fn foo(&self) -> Helper<impl MyOtherTrait, String>;
}
// BAD, Default is not dyn compatible.
trait Trait4 {
fn foo(&self) -> impl Default;
}
// BAD, `Box<dyn MyOtherTrait>` is not unsized.
//
// Not allowed because we probably want to just return `Box<dyn MyOtherTrait>`
// rather than `Init<Box<dyn MyOtherTrait>>`.
trait Trait5 {
fn foo(&self) -> Box<impl MyOtherTrait>;
}
}
The compiler implements this by placing two entries in the vtable:
- The
Layoutfor the concrete return type. - A function pointer that takes a
*mut ()and all arguments of the function, and writes the initialized value to the*mut ()pointer. It returns the metadata needed to construct a wide pointer to the value.
Based on this, it generates an unnameable type according to this logic:
#![allow(unused)]
fn main() {
// 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 `MyTrait`
r#self: &'a dyn MyTrait,
arg1: i32,
arg2: &'a str,
}
impl PinInit<dyn MyOtherTrait> for FooInit<'_> {
type Error = Infallible;
fn layout(&self) -> Layout {
self.r#self.vtable.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
)
}
}
}
Whenever you call <dyn MyTrait>::foo, you receive a value of type FooInit.
The compiler generates one such type for each trait method using impl Trait in
return position. It always implements both Init and PinInit.
Trait objects using the above strategy for any method do not implement the
trait itself, since the method returns an initializer, and the initializer does
not implement the target trait. This is a deviation from the established
principle that dyn Trait implements Trait.
Rationale and alternatives
Only Init trait
It may be possible to start by only introducing an Init trait and then introduce a PinInit trait later. I believe that is probably doable in a forwards-compatible manner. This can avoid the complications with #[pin] annotations on struct fields.
Prior art
Rust for Linux
Rust for Linux has been using a crate called pin-init for a while that provides these features. The crate has a macro pin_init! that implements the init syntax. We have used it and it has worked well in real-world projects.
Move constructors
Projects for performing C++ interop have developed several libraries that use very similar logic to the pin-init crate developed for the Linux kernel. For example, there is the moveit crate.
Prototype
The pin-init crate does not come with support for returning impl Trait in dyn Trait methods, but there is a prototype of the crate with support for this. Please see it here:
https://github.com/Rust-for-Linux/pin-init/tree/dev/experimental/dyn
Backlinks
Addendums
Init expression is an extension to Rust language syntax and trait ecosystem that prescribe a in-place initialisation protocol.
Traits definitions that may work better with fallibility
The core trait in the core standard library would be introduced with the following signature.
#![allow(unused)]
#![feature(try_trait_v2)]
fn main() {
/// Infallible initialisation
pub trait Init<Target> {
fn init(self, slot: *mut Target);
}
/// Fallible initialisation
pub trait TryInit<Target> {
type Residual;
type Output: core::ops::Try<Output = (), Residual = Self::Residual>;
fn try_init(self, slot: *mut Target) -> Self::Output;
}
}
init functions with a out-pointer syntax
The core syntax extension is the init closure and init sugar on function signatures by
annotating which type fragment is the type of the destination place.
#![allow(unused)]
fn main() {
fn try_init_func(data: Data)
-> Result<init MyStruct, Error> {
// ^~~~
// an implicit variable binding `out` as destination of emplacement ...
// which has a type `MyStruct`.
// It looks as if the following user variable declaration is implicitly
// inserted at the beginning of the function call.
// let out: MyStruct;
let processed_data = try_process(data)?;
out.data = processed_data;
// Similar to the out-pointer proposal, the emplacement variable `out`
// has a drop obligation as soon as all the constituents are initialised.
Ok(())
}
/// After lowering the type, which could technically only happen after HIR ...
/// one can inspect the type of the function and this is the actual function
/// "reified" type.
let _: fn(Data) -> impl TryInit<
MyStruct,
Residual = Result<Infallible, Error>,
Output = Result<(), Error>
> = try_init_func;
fn try_init_func2(data: Data) -> Option<init(out) MyStruct> {
out.data = process_opt(data)?;
Some(())
}
let _: fn(Data) -> impl TryInit<
MyStruct,
Residual = Option<Infallible>,
Output = Option<()>
> = try_init_func2;
// To emplace a value, one should invoke the emplacing constructor first,
// and then make use of the trait to perform the actual emplacement.
}
For TryInit functions or associated methods, the TryInit::Residual associated type is inferred
from the return value by replacing the type fragment with a prefix init(..) with Infallible.
For instance, Result<init MyStruct, Error> is desugared into a Residual type
Result<Infallible, Error> and Option<init MyStruct> into Option<Infallible>.
With this type rewrite, TryInit is able to work with Try types to support fallible emplacement
use cases.
NOTE
Following the Rust tradition of biasing towards explicitness, it has been proposed that there should
be a way to control how the output variable can be named.
The notable feature is that the sugar init($ident) indicates that a variable $ident will be made
available with the same type as the one following this syntax fragment.
Therefore, a user should specify which variable would be used as destination to which initialised data should be written.
#![allow(unused)]
fn main() {
fn try_init_func(data: Data)
-> Result<init(res) MyStruct, Error> {
// ^~~
// a variable binding `res` as destination of emplacement.
let processed_data = try_process(data)?;
res.data = processed_data;
// Similar to the out-pointer proposal, the emplacement variable `res`
// has a drop obligation as soon as all the constituents are initialised.
Ok(())
}
}
Box::new with impl Init
Box::<T>::new would be adapted to allow accepting an U type instead where U: Init<T>,
given that we have an blanket impl<T> Init<T> for T and, in addition,
the selection of impl Init in general favours impl Init by user or environment over the blanket
implementation.
#![allow(unused)]
fn main() {
let _: Box<MyBiggerStruct> = Box::new(init MyBiggerStruct { .. }); // OK
let _: Box<MyBiggerStruct> = Box::new(MyBiggerStruct { .. }); // OK, but this is not emplacing
fn make_box<T: Init<MyBiggerStruct>>(emplace: T) -> Box<MyBiggerStruct> {
Box::new(emplace)
// ^ this is emplacing because `T: Init<MyBiggerStruct>` is favoured
}
// fallible case
fn make_fallible<T: TryInit<MyBiggerStruct, Residual = Result<Infallible, MyError>>>(emplace: T)
-> Result<MyBiggerStruct, MyError>
where MyError: AllocError,
{
Box::try_new_init(emplace) // this is emplacing with fallibility
}
}
This can be made possible because already in today’s Rust type system,
when selecting applicable Init implementations, where bounds are always
preferred over user impl block implementations, which are further
preferred over compiler built-in implementations.
Specifying the emplace argument with the Init/TryInit bound will always make
the compiler to select implementations in this order of precedence, so that
if emplace is an initialising closure it would be used for emplacing; and if
emplace does not have any Init/TryInit implementation in the environment,
the value will be moved into the allocation through a trivial initialising
closure generated by the compiler.
Backlinks
Out pointers
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Out pointers are a fundamental building block of in-place initialisation: at the core level it is hard to imagine any way of building the feature on a concrete machine without bringing in out pointers.
The viewpoint of the out pointer approach to in-place initialisation is therefore that Rust should not attempt to hide the existence of out pointers behind special traits, attributes, or compiler magic but should instead make them available as a first class citizen of the language.
The basic idea is a pair of move-only reference-like types Uninit and InPlace:
struct Uninit<'a, T>(&'a mut MaybeUninit<T>, Invariant<'a>);
struct InPlace<'a, T>(Box<T, InPlaceAlloc<'a>>, Invariant<'a>);
fn init_x<'a>(x: Uninit<'a, X>) -> InPlace<'a, X> { ... }
fn main() {
// Note: we'd actually need generativity here to produce a truly unique lifetime for x.
let mut x = MaybeUninit::<X>::uninit();
let x = init_x(Uninit::from(&mut x));
}
Approaches
- Custom types (as above)
&uninit
Resources
In-place initialization via outptrs, Jul 8 2025
- Introduced
InPlace<T>as aBoxwith custom allocator. - Very influential.
Thoughts on “out”-pointer, Nov 12 2025
Backlinks
&uninit references
Introduce a first-class &uninit T pointer type whose initialisation state is
tracked by the compiler. Make it possible to return a marker proving
full-initialisation status of the &uninit T from functions.
Semantics
An &uninit T is a data pointer pointing to uninitialised, partially
initialised, or fully initialised data. The state of the initialisation is not
queryable using runtime functions; it is tracked at compile-time only.
The &uninit T is reborrowable into another &uninit T. The lifetime 'a in
&'a uninit T is invariant and guaranteed unique. An initialised field of an
&uninit T can be reborrowed as &(mut) Field.
When initially received, be it by creation or as a parameter, the &uninit T is
fully uninitialised. When dropped or reborrowed, the &uninit T drops the T
in place if initialised, or drops all initialised fields of T in place
otherwise.
An &uninit T can be split into multiple &uninit Field references in the same
way as a &mut T can be split into &mut Field references.
An &uninit T can be initialised by writing into it, or by using an
initialisation proof marker Initialised<'_>. The lifetime of 'a of
Initialised<'a> is invariant. A field-wise or fully initialised &uninit T
can be reborrowed into an Initialised<'_>, which resets the &uninit T’s
initialisation status to fully uninitialised without performing Drop in place.
Initialising &uninit T or a local uninitialised T
One-shot initialisation
An &uninit T can be fully initialised by writing to it.
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
*r = T::new();
}
This marks the &uninit T as fully initialised and arms its Drop method.
Equivalently for a local uninitialised T:
#![allow(unused)]
fn main() {
let r: T;
r = T::new();
}
Field-wise initialisation
An &uninit T where T is a struct can be initialised field-by-field using a
special initialisation syntax:
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
r.field1 <- Field1::new();
r.field2 <- Field2::new();
r <- _;
}
Alternatively, reuse normal assignment syntax:
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
r.field1 = Field1::new();
r.field2 = Field2::new();
*r = _;
}
Initialising a field makes the borrow checker track that field’s value as an
individual value. Initialising all fields still tracks the fields as individual
values, ie. it does not consider the &uninit T to yet contain an initialised
T and does not therefore arm the Drop method of T.
The final r <- _; or *r = _; line is therefore required to complete the
initialisation; this finally arms the Drop method of T.
Equivalently for a local uninitialised T:
#![allow(unused)]
fn main() {
let r: T;
r.field1 = Field1::new();
r.field2 = Field2::new();
r = _;
}
Initialisation functions
An &uninit T can be passed to a function as a parameter: the callee will
consider the &uninit T to be fully uninitialised. The callee can signal to the
caller that it has fully initialised the &uninit T by returning an
initialisation proof, here called Initialised<'_>.
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
let proof: Initialised<'_> = init_t(r);
r <- proof;
}
The initialisation proof is “notarised” onto the &uninit T using the r <- proof; syntax. An alternative is to use the standard pointer write syntax:
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
*r = init_t(r);
}
This requires special handling in the compiler as Initialised<'_> is not equal to T.
Another alternative would be to make dropping the proof automatically notarise the &uninit T:
#![allow(unused)]
fn main() {
let r: &uninit T = ?;
init_t(r);
}
This requires special handling in the compiler as dropping of Initialised<'_>
would have to happen immediately on the second line above, and its Drop
implementation would have to find the exact r: &uninit T based on the
invariant and guaranteed unique lifetime that they share, and notarise it.
A local uninitialised T cannot be initialised using an initialisation function
without taking an &uninit T reference to it:
#![allow(unused)]
fn main() {
let r: T;
init_t(&uninit t);
}
Justification
We biased towards denying drops of Initialised<'_> other than use for marking place
initialised because the type is intended for notarisation in the current function call
frame.
For more sophisticated post-initialisation manipulation of the initialised data,
we believe that it is more ergonomic and less error-prone if notarisation happens first
and the access to the data is handed off to other sub-routines via &mut borrows.
Syntax sugar
Having to write out &uninit is a nuisance in most case. Many in-place
initialisation cases are dead-simple. For these cases, it would make sense to
have simple syntax sugar to deal with the nuisance.
One possibility would be to use the magic _ binding on the right-hand side of
an assignment with the meaning of “references the left-hand side”; this
reference would necessarily be a an &uninit T since the left-hand side must be
uninitialised when the right-hand side is being evaluated.
This makes calling constructor functions much more pleasant:
#![allow(unused)]
fn main() {
fn init_r(r: &uninit Struct) -> Initialised<'_> { ... }
let r = init_r(_);
}
This would also work with fields:
#![allow(unused)]
fn main() {
struct Struct {
field1: Field1,
field2: Field2,
}
let r: Struct;
r.field1 = init_field1(_); // &uninit Field1 -> Initialised
r.field2 = init_field2(_); // &uninit Field2 -> initialised
r = _;
}
This also applies to r = _; which now desugars into r = &uninit r: if all of
r’s fields are fully initialised then &uninit r can be reborrowed into an
Initialised<'_>. The initialisation proof can then be assigned into r to
finish its initialisation.
Pros & cons
Pros
-
Out pointers are the way that in-place initialisation actually works on the concrete, on-the-metal level. You cannot make the problem simpler than it actually is.
-
Out pointers are explicit and flexible: initialising functions (constructors) are free to choose their calling convention, and functions taking multiple out pointers are not an issue.
-
Initialisation proofs enable making
&uninit Treborrowable: an alternative approach of returning&init Tpointers requires&uninit Tto be aMove-only type. This also enables very efficient initialisation function APIs.
Cons
-
&uninit TandInitialised<'_>are often explicitly spelled out; they are more verbose than automatic solutions. Syntax sugar helps a lot though. -
The implementation requires a non-trivial amount of new compiler features.
- The characterisation of
Initialised<'_>is that every instance must be discharged eventually, implying that the borrow-checker must also check for this at each return point. - In some cases, it will require compiler to perform optimisation to eliminate panicking.
- The characterisation of
-
&uninit Tdoes not itself provide a direct path to solving eg. in-placeBoxorRcinitialisation. The most direct solution of adding new APIs that take animpl FnOnce(&'a uninit T) -> Initialised<'a>run into the same issues asimpl Initdoes: return type of theFnOncespills into the return type of the new APIs, and the new APIs will need various variants to match status quo, leading to API bloat.
NOTE
On the note of enforcing no-drop of Initialised<'_>, the back-up proposal,
if this property would be found objectionable, is to weaken this requirement and
adopt a type that still carries a lifetime with invariance as well as the same
drop implementation of the emplaced type T.
This is also known as &'_ init T or &'_ own T type, because the model will
require the type to contain the pointer to the place under emplacement in order
for dropping to take place.
Examples
Prospective library extension
Out-pointers do not themselves solve the question how smart pointers like Box
would perform in-place initialisation.
However, with sensible library design we believe that out-pointers can provide
better ergonomics when it comes to balancing between convenience, safety, and
flexibility.
#![allow(unused)]
fn main() {
impl<'a, T, A> BoxBuilder<'a, T, A> {
pub fn new_init(alloc: A) -> (&'a uninit T, Self) {
let ptr = if T::IS_ZST {
NonNull::dangling()
} else {
let layout = Layout::new::<mem::MaybeUninit<T>>();
alloc.allocate(layout).unwrap().cast()
};
(ptr as &'a uninit _, Self { ptr, alloc })
}
/// An example for fallible allocation, take 2.
pub fn new_try_init(alloc: A) -> Result<(&'a uninit T, Self), AllocError> {
let ptr = if T::IS_ZST {
NonNull::dangling()
} else {
let layout = Layout::new::<mem::MaybeUninit<T>>();
alloc.allocate(layout)?.cast()
};
Ok((ptr as &'a uninit _, Self { ptr, alloc }))
}
/// An example for fallible allocation.
pub fn new_opt_init(alloc: A) -> Option<(&'a uninit T, Self)> {
let ptr = if T::IS_ZST {
NonNull::dangling()
} else {
let layout = Layout::new::<mem::MaybeUninit<T>>();
alloc.allocate(layout).ok()?.cast()
};
Some((ptr as &'a uninit _, Self { ptr, alloc }))
}
pub fn finalise(self, initialize: Initialized<'a>) -> Box<T, A> {
unsafe {
// Safety: discharge initialize because we are going to set
// the Unique as initialized
initialize.discharge();
// Safety: we make a switch on the init state now.
Box::from_raw_in(self.ptr, self.alloc)
}
}
}
struct Struct {
data1: [u8; 32],
data2: [u8; 32],
}
let (uninit_struct, builder) = BoxBuilder::<'_, Struct>::new_init(Global);
uninit_struct.data1 <- [0; 32];
uninit_struct.data2 <- [4; 32];
let box_struct = builder.finalise(uninit_struct);
/// With this, convenient and opinionated Box emplacing constructors can be
/// built.
impl<T, A> Box<T, A> {
pub fn new_init(
alloc: A,
ctor: impl for<'a> FnOnce(&'a uninit T) -> Initialised<'a>,
) -> Self
{
let (uninit_struct, builder) = BoxBuilder::<'_, T>::new_init(alloc);
let initialize = ctor(&uninit_struct);
builder.finalise(initialize)
}
pub fn new_opt_init(
alloc: A,
ctor: impl for<'a> FnOnce(&'a uninit T) -> Option<Initialised<'a>>,
) -> Option<Self>
{
let (uninit_struct, builder) = BoxBuilder::<'_, T>::new_opt_init(alloc)?;
ctor(&uninit_struct).map(|initialize| {
builder.finalise(initialize)
})
}
pub fn new_try_init<F, E>(
alloc: A,
ctor: F,
) -> Result<Self, E>
where
F: for<'a> FnOnce(&'a uninit T) -> Result<Initialised<'a>, E>,
E: 'static + From<AllocError>,
{
let (uninit_struct, builder) = BoxBuilder::<'_, T>::new_try_init(alloc)?;
Ok(builder.finalise(ctor(&uninit_struct)?))
}
}
}
Correct usage
These are examples of correct and recommended usage.
One-shot initialisation
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
fn init_s(s: &uninit Struct) -> Initialised<'_> {
*s = Struct::default();
s
}
let s = init_s(_);
}
Field-wise initialisation
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
fn init_b(s: &uninit Box<u32>) -> Initialised<'_> {
*s = Default::default();
}
let s: Struct;
s.field1 = init_b(_);
s.field2 = init_b(_);
s = _;
}
C++ constructor ABI
A C++ constructor takes an *mut self parameter and returns it as (hopefully)
initialised. Implementing the equivalent with &uninit T is possible, though it
requires either that Initialised<'_> can be wrapped in ManuallyDrop (so it
must not be a true linear type), or that the planned unsafe fn drop_in_place
overriding feature is used.
#![allow(unused)]
fn main() {
struct Class { ... };
#[repr(transparent)]
struct Init<'a, T>(&'a uninit T, Initialised<'a>);
impl Destruct for Init<'_, T> {
unsafe fn drop_in_place(&mut self) {
// NOTE: self.0 is considered fully uninitialised here.
// Mark self.0 as fully initialised: how to move out of self.1 though?
self.0 = self.1;
// Here self.0 is exiting the function and gets dropped in place.
}
}
extern "C" fn lib__Class__new(c: &mut uninit Class) -> Init<'_, Class> {
c.field1 = init_field1(_);
// ... other field inits here ...
// notarise field-wise initialised &uninit Class, arming its `Drop` and then
// immediately moving the Drop responsibility into proof.
let proof: Initialised = c;
// NOTE: c is now considered fully uninitialised as proof carries Drop
// responsibility. Class is not uninitialised here.
Init(c, proof)
}
}
Calling a C++ constructor
#![allow(unused)]
fn main() {
struct Class { ... };
#[repr(transparent)]
struct Init<'a, T>(&'a uninit T, Initialised<'a>);
impl Destruct for Init<'_, T> { ... } // same as above
impl<'a, T> Init<'a, T> {
fn into_proof(self) -> Initialised<'a> {
// NOTE: self.0 is considered fully uninitialised here.
self.1
// NOTE: self.0 is still considered fully uninitialised here and thus no
// drop in place is performed.
}
}
#[link(name = "lib")]
unsafe extern "C" {
fn lib__Class__new<'a>(&'a uninit Class) -> Init<'a, Class>;
}
let c: Class = unsafe { lib__Class__new(_) }.into_proof();
}
Fallible initialisation
#![allow(unused)]
fn main() {
fn try_init_s(s: &uninit Struct) -> Result<Initialised<'_>, dyn Error> { ... }
let s = try_init_s(_)?;
}
Multiple out pointers
#![allow(unused)]
fn main() {
fn init_field1_and_field2<'a, 'b>(
v: u32,
field1: &'a uninit Field1,
field2: &'b uninit Field2
) -> (Initialised<'a>, Initialised<'b>) { ... }
let s: Struct;
init_field1_and_field2(3, &uninit s.field1, &uninit s.field2);
}
Incorrect usage examples
These are examples of incorrect usage that must not compile.
Reference to partially initialised struct
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
fn init_b(s: &uninit Box<u32>) -> Initialised<'_> {
*s = Default::default();
}
let s: Struct;
s.field1 = init_b(_);
let: &Struct = &s; // ERROR: used binding `s` isn't initialized
s.field2 = init_b(_);
let: &Struct = &s; // ERROR: used binding `s` isn't initialized
s = _;
let: &Struct = &s; // OK
}
Misuse examples
These are examples of correct but problematic usage: they compile but contain mistakes.
Partial initialisation undone
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
fn init_s(s: &uninit Struct) -> Initialised<'_> {
// NOTE: s is considered fully uninitialised at function entry here.
*s = Struct::default();
s
}
let s: Struct;
s.field1 = Default::default();
// MISTAKE: field1 was initialised and is dropped here.
s = init_s(_);
}
Partial initialisation undone by return
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
fn half_init_s(s: &uninit Struct) {
s.field1 = Default::default();
// MISTAKE: there is no way to return the half-initialised state. Thus,
// field1 is dropped here.
}
let s: Struct;
half_init_s(&uninit s);
s = Default::default();
}
Field-wise initialisation unfinished
#![allow(unused)]
fn main() {
struct Struct {
field1: Box<u32>,
field2: Box<u32>,
}
impl Drop for Struct {
fn drop(&mut self) {
eprintln!("Dropped");
}
}
fn init_b(s: &uninit Box<u32>) -> Initialised<'_> {
*s = Default::default();
}
let s: Struct;
s.field1 = init_b(_);
s.field2 = init_b(_);
// MISTAKE: s is never notarised and thus "Dropped" will not be printed.
}
API sketch
Backlinks
Placing functions
This proposal provides an alternative syntax support for emplacement.
A new attribute #[placing] (placeholder notation) is introduced on the ADT type and function declaration site to signal
to the compiler that the type, function or closure is involved in emplacement and the appropriate
transformation is applied to support emplacement.
Type annotation
This attribute at item location wraps the original ADT in a MaybeUninit and generate proper
destructors.
#![allow(unused)]
fn main() {
#[placing]
struct Data {
value: Value,
}
// ... generates code equivalent to the following.
#[repr(transparent)]
struct Data(MaybeUninit<DataInner>);
struct DataInner {
value: Value,
}
impl Drop for Data {
fn drop(&mut self) {
// Safety: generated `#[placing]` functions will never call this
// unless initialisation is fully completed.
unsafe {
self.0.assume_init_drop();
}
}
}
}
Constructor annotation
The same attribute but at function location demands that the return value to carry the #[placing]
attribute.
Given that, there is further code expansion so that these functions are emplacement constructors.
#![allow(unused)]
fn main() {
#[placing]
impl Data {
#[placing]
fn new() -> Data {
Data {
value: make_value(),
}
}
}
let _: Data = Data::new(); // OK
// ... generates code equivalent to the following.
impl Data {
// The following function is generated exactly once
unsafe fn new_uninit() -> Self {
Self(MaybeUninit::uninit())
}
// This function initialises the value in-place.
// Safety:
// - This function shall not be called more than once.
// - This function can only be called on value generated from `new_uninit`.
unsafe fn new(&mut self) -> Data {
let this = self.0.as_mut_ptr();
unsafe {
(&raw mut (*this).value).write(make_value());
}
}
}
}
Method annotations
Inherent and trait associated methods within impl blocks annotated with #[placing] have
transparent access to fields with the help of the rewrite.
#![allow(unused)]
fn main() {
#[placing]
impl Data {
fn get_value(&self) -> &Value {
&self.value
}
fn set_value(&mut self, value: Value) {
self.value = value;
}
}
// ... generates code equivalent to the following
impl Data {
fn get_value(&self) -> &Value {
// Safety:
// - This function can only be called after `new` is called.
let this = unsafe { self.0.assume_init_ref() };
// Here goes the rest of the original function:
&this.value
}
fn set_value(&mut self, value: Value) {
// Safety:
// - This function can only be called after `new` is called.
let this = unsafe { self.0.assume_init_mut() };
// Here goes the rest of the original function:
this.value = value;
}
}
}
Resources
placing functions, July 2025
Placing Arguments, August 2025
Backlinks
Guaranteed value emplacement
Note
This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.
Resources
RFC Draft: Guaranteed Value Emplacement - HackMD
Backlinks
Running Examples
This chapter regroups examples we discuss regularly:, so that we can be clear about the intended APIs etc.
ArcBox
An ArcBox is a Box backed with a reference-counted storage.
It supports field projections, on top of everything else that Box should have.
#![allow(unused)]
fn main() {
/// A `Box` backed with a reference-counted storage. Supports field projection.
// Safety: we're the unique pointer to this `T`.
// If any field is borrowed by an `ArcBoxMap`, then this value is no longer safe, obviously.
pub struct ArcBox<T>(Arc<T>);
/// A subplace of an `ArcBox`.
pub struct ArcBoxMap<T> {
/// Pointer to the reference counts. This keeps the backing storage allocated while any
/// `ArcBoxMap` to it exists.
header: NonNull<ArcHeader>,
/// Pointer to the subplace we care about.
val: NonNull<T>,
}
// Both support: read, write, move out, drop, borrow fields as `&`, `&mut`
// Both also support projecting to `ArcBoxMap`.
impl<T> ArcBox<T> {
pub fn new(x: T) -> Self { .. }
// Implemented trivially, since we can only get a full `self` if none of the fields are borrowed
// by an `ArcBoxMap`.
pub fn into_arc(self) -> Arc<T> { .. }
}
// `ArcBox` has no need for a `Drop` impl, it reuses the one from `Arc`.
// Drops the pointed-to value, decrements the refcount.
impl Drop for ArcBoxMap { .. }
}
Example usage:
#![allow(unused)]
fn main() {
let x: ArcBox<Foo> = ...;
let field = @ArcBoxMap x.field;
do_something(&mut x.other_field);
do_something(&mut field.whatever);
// at end of scope:
// - `field` drops normally;
// - dropck must drop `x.other_field`;
// - dropck must dispose of the empty `ArcBox` (that's not its normal `Drop`).
}
UniqueArc
UniqueArc is an Arc while it is known to be uniquely owned. Typically used for initialization,
after which it can be turned into a normal Arc.
#![allow(unused)]
fn main() {
// Safety: we're the unique pointer to this `T`.
// While this is live, the strong count is 0 so that we can give out other `Weak` pointers to this
// and prevent them from being upgraded.
pub struct UniqueArc<T>(Weak<T>);
// Supports: read, write, borrow fields as `&`, `&mut`
impl<T> UniqueArc<T> {
pub fn new(x: T) -> Self { .. }
pub fn downgrade(&self) -> Weak<T> { .. }
// This sets the strong count to 1.
pub fn into_arc(self) -> Arc<T> { .. }
}
}
ACP 700 proposes a way to field-project
a UniqueArc: first we change how strong counts work. Instead of just a strong count, we cram two
counters into the u64, one for shared refs and one for unique refs, and update the rest of the
logic accordingly. Can’t upgrade a weak pointer if there are any unique refs.
#![allow(unused)]
fn main() {
impl<T> UniqueArc<T> {
// Checks the unique-ptr count.
pub fn try_into_arc(self) -> Result<Arc<T>, NotUnique> { .. }
}
/// A subplace of a `UniqueArc`.
pub struct UniqueArcMap<T> {
/// Pointer to the reference counts.
header: NonNull<ArcHeader>,
/// Pointer to the subplace we care about.
val: NonNull<T>,
}
// Supports: read, write, borrow fields as `&`, `&mut`, reborrow fields as `UniqueArcMap`
}
The issue then is:
- If we don’t give
UniqueArcMapspecial borrowck behavior, then disjoint borrowing must be done with methods that split the borrow; that’s sad. - If we do give
UniqueArcMapspecial borrowck behavior, then we can prevent multiple@UniqueArcMap x.fieldto the same fields. However, we can’t get back aArc<T>anymore:
#![allow(unused)]
fn main() {
let x: UniqueArc<Foo> = ...;
let field = @UniqueArcMap x.field;
// can't access `x.field` anymore
// in particular, can't call a method on `x` itself, since that may access `x.field`:
let arc = x.try_into_arc()?; // ERROR `x.field` is borrowed
}
SharedMutableRef
This is a funky counter-example to how one might think borrows must chain: it’s a shared reference through which you can write.
Writing ability is justified by the !Send and !Sync impls: this can only be used on a single
thread.
#![allow(unused)]
fn main() {
struct SharedMutableRef<'a, T>(NonNull<T>, PhantomData<&'a mut T>);
impl !Send for SharedMutableRef<'a, T> {}
impl !Sync for SharedMutableRef<'a, T> {}
// Supports: local T -> SharedMutableRef<T> exclusive borrow
// Supports: &mut T -> SharedMutableRef<T> exclusive reborrow
// Supports: SharedMutableRef<T> -> SharedMutableRef<T> *shared* reborrow
// Supports: read, write
// Importantly doesn't support &T or &mut T reborrows
}
Example usage:
#![allow(unused)]
fn main() {
let mut x = 42;
let weird = @SharedMutableRef x;
let reborrow1 = @SharedMutableRef *weird;
let reborrow2 = @SharedMutableRef *weird; // look it's a shared ref
*reborrow1 += 1; // hehe
*reborrow2 += 1;
// `x` is inaccessible while any of the above refs are live
}