Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

#t-lang/custom-refs

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.


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


Field projections

Note

This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.

Resources

#t-lang/custom-refs

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


Place expression desugaring

Note

This article is a stub. Help improve the wiki by clicking the edit icon above and submitting a pull request.


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


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 ()>);
}

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> { /* ... */ }
}

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 ()>);
}

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>,
}
}

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.

  1. The type must not be Clone or Copy.
  • Alternatively, blanket-implement Reborrow for T: Copy, but Clone + !Copy types cannot be Reborrow.
  • This limitation is placed to avoid types that are both Copy and Reborrow, as that only makes sense if Copy types are considered a “base case” for recursive reborrowing.
  • Clone + !Copy types generally only make sense if a corresponding Drop implementation exists, and reborrowing cannot be soundly combined with Drop. If a type is Clone + !Copy + !Drop then 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).
  1. The type must not have a Drop implementation.
  • 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.
  1. The type must have at least one lifetime.
  • Alternatively, if Copy types blanket-implement Reborrow then this limitation cannot be placed, but manually deriving Reborrow on 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.
  1. The result of a Reborrow operation 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 Reborrow returns 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 T bottoms out and performs a reborrow on 'a
  • CustomMut<'a, T> can be assumed to bottom out and reborrow on 'a.
  • Two<'a, 'b> needs to be checked: does it reborrow both 'a and 'b or only one of them?
  • PhantomData<&'a ()> bottoms out and DOES NOT perform a reborrow on 'a: this is a Copy marker.
  • PhantomExclusive<'a> bottoms out and performs a reborrow on 'a.

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.


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.


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.

  1. The type implementing CoerceShared must also implement Reborrow.
  • Coercing an exclusive reference into a shared reference doesn’t make sense if the source type is not an exclusive reference.
  1. The result of the CoerceShared operation must be a Copy type.
  • CoerceShared can 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 perform CoerceShared once and produce copies of that result.
  1. The result of the CoerceShared operation must have at least one lifetime.
  • A lifetime-less type cannot contain the lifetime information that sound reborrowing relies upon.
  1. The lifetime of the result must be equivalent to the source.
  • If CoerceShared returns 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.
  1. 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.


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 ...
}

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


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:

  1. Merge the HasPlace and Receiver traits.
  2. Make Receiver a supertrait of HasPlace.
  3. Make HasPlace a supertrait of Receiver.
  4. 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.

  1. 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
        }
    })
    }
  2. If they are separate, we compute the list
    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,
            ...
        ],
        ...
    ]
    
    The computation of this list is described by this code snippet:
    #![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 X occurs in the candidate list that we walked to arrive at this method, we let q := *...*p be suitably derefed to get to X, which is the number of HasPlace::Target we go through. We then desugar the method to U::method(q) or <U as Trait>::method(q).
  • If X does not occur in the already considered candidates then X: HasPlace must be true. If that’s not the case, we emit an error.
    • If X::Target occurs in the already considered candidate, we then let q := *...*p be suitably derefed to get to X::Target. We then desugar to U::method(@X q) or <U as Trait>::method(@X q).
    • If X::Target does not occur in the list of already considered candidates, then we continue with the next impl or type from the candidate list.

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 HasPlace chain. An alternative would be to implement Deref for 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)
  • 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

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)
  • Example
    • found inherent fn method(self: ArcRef<Self>)
      • X = ArcRef<Example> not found in the candidate list, but X: HasPlace
      • X::Target == Example found at index 1 in candidate list,
        • => one deref is added and borrow using ArcRef

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)
  • Example
    • found inherent fn method(self: &ArcRef<Self>)
      • X = &ArcRef<Example> not found in the candidate list, but X: HasPlace
      • X::Target == ArcRef<Example> not found in candidate list
        • => continue with next impl/type
    • found trait fn method(self: &Self) in Trait
      • X = &Example not found in the candidate list, but X: HasPlace
      • X:: Target == Example found at index 1 in candidate list,
        • => one deref is added and borrow using &

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, but X: HasPlace
      • X::Target == Example not found in candidate list (we only check up to the point where we currently are at!)
        • => continue with next impl/type
  • Example
    • no impl block contains a fn method(self: X)
  • 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.example is desugared to @%MaybeUninit (**parent).example, which has the type MaybeUninit<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)
  • Example
    • found inherent fn method(self: Pin<&mut MaybeUninit<Self>>)
      • X = Pin<&mut MaybeUninit<Example>> not found in candidate list, but X: HasPlace
      • X::Target == MaybeUninit<Example> found in candidate list at index 0
        • => no derefs are added and borrow using Pin<&mut MaybeUninit<Example>>

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)
  • Box<Box<Box<Example>>>
    • no impl block contains a fn method(self: X)
  • Box<Box<Example>>
    • no impl block contains a fn method(self: X)
  • Box<Example>
    • no impl block contains a fn method(self: X)
  • Example
    • found inherent fn method(self: &Self)
      • X = &Example not found in candidate list, but X: HasPlace
      • X::Target == Example found in candidate list at index 4
        • => 4 derefs are added and borrow using &

Resources


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 Result or an Option.
  • 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 panic in 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 type MyStruct into an emplacing initialiser of a type Mutex<MyStruct>.
  • Good composibility with any control-flow.

    • This covers control-flow constructs such as early returns via the try ? operator, coroutine suspension yield and async future suspension await as well as the break, continue keywords usable in loops
  • 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.
  • Minimal changes to smart pointer and container APIs for emplacement.

    • Ideally we have seamless support by Box and Vec as 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.
  • 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 Zeroable trait 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 Zeroable trait to signal a specific way to fallback during emplacement, in case a struct field is left unspecified. In pin-init crate syntax the trait is used at a symbolic .. struct base expression, so that the unmentioned fields are filled with zeros through the Zeroable::init_zeroed() method.
  • 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 Try such as Result, Option and Poll
      • const and in association the host effect under the theme of constant-time evaluation
      • async, async gen and gen under the theme of coroutines
      • Pined-ness in the theme of self-referential data
      • … and combination of all function flavours above
    • A successful proposal should demonstrate good synergy with the mentioned “effects” or at least prospect of future extensions to support “effects” and “modifications.”
  • 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

#t-lang/in-place-init

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.


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 PinInit for 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:

  1. Construct value on the stack.
  2. Allocate memory on the heap.
  3. Move value from 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:

  1. Allocate memory on the heap.
  2. Construct value directly 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::new in 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: rhs
  • name @ 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 Layout for 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


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.


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

Resources

In-place initialization via outptrs, Jul 8 2025

  • Introduced InPlace<T> as a Box with custom allocator.
  • Very influential.

Thoughts on “out”-pointer, Nov 12 2025


&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

  1. 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.

  2. 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.

  3. Initialisation proofs enable making &uninit T reborrowable: an alternative approach of returning &init T pointers requires &uninit T to be a Move-only type. This also enables very efficient initialisation function APIs.

Cons

  1. &uninit T and Initialised<'_> are often explicitly spelled out; they are more verbose than automatic solutions. Syntax sugar helps a lot though.

  2. 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.
  3. &uninit T does not itself provide a direct path to solving eg. in-place Box or Rc initialisation. The most direct solution of adding new APIs that take an impl FnOnce(&'a uninit T) -> Initialised<'a> run into the same issues as impl Init does: return type of the FnOnce spills 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


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


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


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 UniqueArcMap special borrowck behavior, then disjoint borrowing must be done with methods that split the borrow; that’s sad.
  • If we do give UniqueArcMap special borrowck behavior, then we can prevent multiple @UniqueArcMap x.field to the same fields. However, we can’t get back a Arc<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
}