This pre-RFC has resulted in this RFC.
- Feature Name:
field_projection
- Start Date: 2022-09-10
- RFC PR: rust-lang/rfcs#3318
- Rust Issue: rust-lang/rust#0000
Summary
The stdlib has wrapper types that impose some restrictions/additional features
on the types that are wrapped. For example: MaybeUninit<T>
allows T
to be
partially initialized. These wrapper types also affect the fields of the types.
At the moment there is no easy access to these fields.
This RFC proposes to add field projection to certain wrapper types from the
stdlib:
from | to |
---|---|
& MaybeUninit <Struct>
|
& MaybeUninit <Field>
|
& Cell <Struct>
|
& Cell <Field>
|
& UnsafeCell <Struct>
|
& UnsafeCell <Field>
|
Option <&Struct>
|
Option <&Field>
|
Pin <&Struct>
|
Pin <&Field>
|
Pin <& MaybeUninit <Struct>>
|
Pin <& MaybeUninit <Field>>
|
Other pointers are also supported, for a list, see here.
Motivation
Currently, there are some map functions that provide this functionality. These functions are not as ergonomic as a normal field access would be:
struct Count {
inner: usize,
outer: usize,
}
fn do_stuff(debug: Option<&mut Count>) {
// something that will be tracked by inner
if let Some(inner) = debug.map(|c| &mut c.inner) {
*inner += 1;
}
// something that will be tracked by outer
if let Some(outer) = debug.map(|c| &mut c.outer) {
*inner += 1;
}
}
With this RFC this would become:
struct Count {
inner: usize,
outer: usize,
}
fn do_stuff(debug: Option<&mut Count>) {
// something that will be tracked by inner
if let Some(inner) = &mut debug.inner {
*inner += 1;
}
// something that will be tracked by outer
if let Some(outer) = &mut debug.outer {
*inner += 1;
}
}
While this might only seem like a minor improvement for Option
<T>
it is transformative for Pin
<P>
and
MaybeUninit
<T>
:
struct Count {
inner: usize,
outer: usize,
}
fn init_count(mut count: Box<MaybeUninit<Count>>) -> Box<Count> {
let inner: &mut MaybeUninit<usize> = count.inner;
inner.write(42);
count.outer.write(63);
unsafe {
// SAFETY: all fields have been initialized
count.assume_init() // #![feature(new_uninit)]
}
}
Before, this had to be done with raw pointers!
Pin
<P>
has a similar story:
struct RaceFutures<F1, F2> {
// Pin is somewhat special, it needs some way to specify
// structurally pinned fields, because `Pin<&mut T>` might
// not affect the whole of `T`.
#[pin]
fut1: F1,
#[pin]
fut2: F2,
}
impl<F1, F2> Future for RaceFutures<F1, F2>
where
F1: Future,
F2: Future<Output = F1::Output>,
{
type Output = F1::Output;
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
match self.fut1.poll(ctx) {
Poll::Pending => self.fut2.poll(ctx),
rdy => rdy,
}
}
}
Without this proposal, one would have to use unsafe
with
Pin::map_unchecked_mut
to project the inner fields.
Guide-level explanation
MaybeUninit
<T>
When working with certain wrapper types in rust, you often want to access fields
of the wrapped types. When interfacing with C one often has to deal with
uninitialized data. In rust uninitialized data is represented by
MaybeUninit
<T>
. In the following example we demonstrate
how one can initialize partial fields using MaybeUninit
<T>
.
#[repr(C)]
pub struct MachineData {
incident_count: u32,
device_id: usize,
device_specific: *const core::ffi::c_void,
}
extern "C" {
// provided by the C code
/// Initializes the `device_specific` pointer based on the value of `device_id`.
/// Returns -1 on error (unknown id) and 0 on success.
fn lookup_device_ptr(data: *mut MachineData) -> i32;
}
pub struct UnknownId;
impl MachineData {
pub fn new(id: usize) -> Result<Self, UnknownId> {
let mut this = MaybeUninit::<Self>::uninit();
// the type of `this.device_id` is `MaybeUninit<usize>`
this.device_id.write(id);
this.incident_count.write(0);
// SAFETY: ffi-call, `device_id` has been initialized
if unsafe { lookup_device_ptr(this.as_mut_ptr()) } != 0 {
Err(UnknownId)
} else {
// SAFETY: all fields have been initialized
Ok(unsafe { this.assume_init() })
}
}
}
So to access a field of MaybeUninit
<MachineData>
we can use
the already familiar syntax of accessing a field of MachineData
/&MachineData
/&mut MachineData
. The difference is that the type of the expression
this.device_id
is now MaybeUninit
<usize>
.
These field projections are also available on other types.
Pin
<P>
projections
Our second example is going to focus on Pin
<P>
. This type is a little
special, as it allows unwrapping while projecting, but only for specific fields.
This information is expressed via the #[unpin]
attribute on the given field.
struct RaceFutures<F1, F2> {
fut1: F1,
fut2: F2,
// this will be used to fairly poll the futures
#[unpin]
first: bool,
}
impl<F1, F2> Future for RaceFutures<F1, F2>
where
F1: Future,
F2: Future<Output = F1::Output>,
{
type Output = F1::Output;
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
// we can access self.first mutably, because it is `#[unpin]`
self.first = !self.first;
if self.first {
// `self.fut1` has the type `Pin<&mut F1>` because `fut1` is a pinned field.
// if it was not pinned, the type would be `&mut F1`.
match self.fut1.poll(ctx) {
Poll::Pending => self.fut2.poll(ctx),
rdy => rdy,
}
} else {
match self.fut2.poll(ctx) {
Poll::Pending => self.fut1.poll(ctx),
rdy => rdy,
}
}
}
}
Defining your own wrapper type
First you need to decide what kind of projection your wrapper type needs:
- field projection: this allows users to project
&mut Wrapper<Struct>
to&mut Wrapper<Field>
, this is only available on types with#[repr(transparent)]
- inner projection: this allows users to project
Wrapper<&mut Struct>
toWrapper<&mut Field>
, this is not available forunion
s
Field projection
Annotate your type with #[field_projecting($T)]
where $T
is the
generic type parameter that you want to project.
#[repr(transparent)]
#[field_projecting(T)]
pub union MaybeUninit<T> {
uninit: (),
value: ManuallyDrop<T>,
}
Inner projection
Annotate your type with #[inner_projecting($T, $unwrap)]
where
-
$T
is the generic type parameter that you want to project. -
$unwrap
is an optional identifier, that - when specified - is available to users to allow projecting fromWrapper<Pointer<Struct>> -> Pointer<Field>
on fields marked with#[$unwrap]
.
#[inner_projecting(T)]
pub enum Option<T> {
Some(T),
None,
}
Here is Pin
as an example with $unwrap
:
#[inner_projecting(T, unpin)]
pub struct Pin<P> {
pointer: P,
}
// now the user can write:
struct RaceFutures<F1, F2> {
fut1: F1,
fut2: F2,
#[unpin]
first: bool,
}
&mut race_future.first
has type &mut bool
, because it is marked by #[unpin]
.
Reference-level explanation
Here is the list of types from core
that will be field_projecting
:
These will be inner_projecting
:
Supported pointers
These are the pointer types that can be used as P
in P<Wrapper<Struct>> -> P<Wrapper<Field>>
for field_projecting
and in Wrapper<P<Struct>> -> Wrapper<P<Field>>
for inner_projecting
:
-
&mut T
,&T
-
*mut T
,*const T
,NonNull<T>
,AtomicPtr<T>
-
Pin<P>
whereP
is from above
Note that all of these pointers have the same size and all can be transmuted to
and from *mut T
. This is by design and other pointer types suggested should
follow this. There could be an internal trait
trait NoMetadataPtr<T> {
fn into_raw(self) -> *mut T;
/// # Safety
/// The supplied `ptr` must have its origin from either `Self::into_raw`, or
/// be directly derived from it via field projection (`ptr::addr_of_mut!((*raw).field)`)
unsafe fn from_raw(ptr: *mut T) -> Self;
}
that then could be used by the compiler to ease the field projection implementation.
Implementation
Add two new attributes: #[field_projecting($T)]
and #[inner_projecting($T)]
both taking a generic type parameter as an argument.
#[field_projecting($T)]
Restrictions
This attribute is only allowed on #[repr(transparent)]
types where the only
field has the layout of $T
. Alternatively the type is a ZST.
How it works
This is done, because to do a projection, the compiler will
mem::transmute::<&mut Wrapper<Struct>, *mut Struct>
and then get the field
using ptr::addr_of_mut!
after which the pointer is then again
mem::transmute::<*mut Field, &mut Wrapper<Field>>
d to yield the
projected field.
#[inner_projecting($T, $unwrap)]
Restrictions
This attribute cannot be added on union
s, because it is unclear what field
the projection would project. For example:
#[inner_projecting($T)]
pub union WeirdPair<T> {
a: (ManuallyDrop<T>, u32),
b: (u32, ManuallyDrop<T>),
}
$unwrap
can only be specified on #[repr(transparent)]
, because otherwise
Wrapper<Pointer<Struct>>
cannot be projected to Pointer<Field>
.
How it works
Each field mentioning $T
will either need to be a ZST, or #[inner_projecting]
or $T
.
The projection will work by projecting each field of type Pointer<$T>
(remember, we
are projecting from Wrapper<Pointer<Struct>> -> Wrapper<Pointer<Field>>
) to
Pointer<$F>
and construct a Wrapper<Pointer<$F>>
in place (because Pointer<$F>
will have the same size as Pointer<$T>
this will take up the same number of
bytes, although the layout might be different). The last step will be skipped if
the field is marked with #[$unwrap]
.
Interactions with other language features
Bindings
Bindings could also be supported:
struct Foo {
a: usize,
b: u64,
}
fn process(x: &Cell<Foo>, y: &Cell<Foo>) {
let Foo { a: ax, b: bx } = x;
let Foo { a: ay, b: by } = y;
ax.swap(ay);
bx.set(bx.get() + by.get());
}
This also enables support for enum
s:
enum FooBar {
Foo(usize, usize),
Bar(usize),
}
fn process(x: &Cell<FooBar>, y: &Cell<FooBar>) {
use FooBar::*;
match (x, y) {
(Foo(a, b), Foo(c, d)) => {
a.swap(c);
b.set(b.get() + d.get());
}
(Bar(x), Bar(y)) => x.swap(y),
(Foo(a, b), Bar(y)) => a.swap(y),
(Bar(x), Foo(a, b)) => b.swap(x),
}
}
They however seem not very compatible with MaybeUninit
<T>
(more work needed).
Pin projections
Because Pin
<P>
is a bit special, as it is the only Wrapper that
permits access to raw fields when the user specifies so. It needs a mechanism
to do so. This proposal has chosen an attribute named #[unpin]
for this purpose.
It would only be a marker attribute and provide no functionality by itself.
It should be located either in the same module so ::core::pin::unpin
or at the
type itself ::core::pin::Pin::unpin
.
There are several problems with choosing #[unpin]
as the marker:
- poor migration support for users of pin-project
- not yet resolved the problem of
PinnedDrop
that can be implemented more easily with#[pin]
, see below.
Alternative: specify pinned fields instead (#[pin]
)
An additional challenge is that if a !Unpin
field is marked #[pin]
, then
one cannot implement the normal Drop
trait, as it would give access to
&mut self
even if self
is pinned. Before this did not pose a problem, because
users would have to use unsafe
to project !Unpin
fields. But as this
proposal makes this possible, we have to account for this.
The solution is similar to how pin-project solves this issue: Users are not
allowed to implement Drop
manually, but instead can implement PinnedDrop
:
pub trait PinnedDrop {
fn drop(self: Pin<&mut Self>);
}
similar to Drop::drop
, PinnedDrop::drop
would not be callable by normal code.
The compiler would emit the following Drop
stub for types that had #[pin]
ned
fields and a user specified PinnedDrop
impl:
impl Drop for $ty {
fn drop(&mut self) {
// SAFETY: because `self` is being dropped, there exists no other reference
// to it. Thus it will never move, if this function never moves it.
let this = unsafe { ::core::pin::Pin::new_unchecked(self) };
<Self as ::core::ops::PinnedDrop>::drop(this)
}
}
To resolve before merge:
We could of course set an exception for pin
and mark fields that keep the
wrapper in contrast to other types. But Option
does not support projecting
"out of the wrapper" so this seems weird to make a general option.
Drawbacks
- Users currently relying on crates that facilitate field projections (see prior art) will have to refactor their code.
- Increased compiler complexity:
- longer compile times
- potential worse type inference
Rationale and alternatives
This RFC consciously chose the presented design, because it addresses the following core issues:
- ergonomic field projection for a wide variety of types with user accesible ways of implementing it for their own types.
- this feature integrates well with itself and other parts of the language.
- the field access operator
.
is not imbued with additional meaning: it does not introduce overhead to use.
on&mut MaybeUninit<T>
compared to&mut T
.
In particular this feature will not and should not in the future support
projecting types that require additional maintenance like Arc
.
This would change the meaning of .
allowing implicit creations of potentially
as many Arc
s as one writes .
.
Out of scope: Arc
projection
With the current design of Arc
it is not possible to add field projection,
because the refcount lives directly adjacent to the data. Instead the stdlib should
include a new type of Arc
(or ProjectedArc<Field, Struct>
) that allows
projection via a map
function:
pub struct ProjectedArc<T, S> {
backing: Arc<S>,
ptr: NonNull<T>,
}
impl<T> Arc<T> {
pub fn project<U>(&self, map: impl FnOnce(&T) -> &U) -> ProjectedArc<U, T> {
ProjectedArc {
backing: self.clone(),
ptr: NonNull::from(map(&**self)),
}
}
}
What other designs have been considered and what is the rationale for not choosing them?
This proposal was initially only designed to enable projecting
Pin
<&mut T>
, because that would remove the need for unsafe
when
pin projecting.
It seems beneficial to also provide this functionality for a wider range of types.
What is the impact of not doing this?
Users of these wrapper types need to rely on crates listed in prior art
to provide sensible projections. Otherwise they can use the mapping functions
provided by some of the wrapper types. These are however, rather unergonomic
and wrappers like Pin
<P>
require unsafe
.
Prior art
Crates
There are some crates that enable field projections via (proc-)macros:
-
pin-project provides pin projections via a proc macro on the type specifying
the structurally pinned fields. At the projection-site the user calls a projection
function
.project()
and then receives a type with each field replaced with the respective projected field. -
field-project provides pin/uninit projection via a macro at the projection-site:
the user writes
proj!($var.$field)
to project to$field
. It works by internally usingunsafe
and thus cannot pin-project!Unpin
fields, because that would be unsound due to theDrop
impl a user could write. -
cell-project provides cell projection via a macro at the projection-site:
the user writes
cell_project!($ty, $val.$field)
where$ty
is the type of$val
. Internally, it uses unsafe to facilitate the projection. - pin-projections provides pin projections, it differs from pin-project by providing explicit projection functions for each field. It also can generate other types of getters for fields. pin-project seems like a more mature solution.
-
project-uninit provides uninit projections via macros at the projection-site
uses
unsafe
internally.
All of these crates have in common that their users have to use macros when they want to perform a field projection.
Other languages
Other languages generally do not have this feature in the same extend. C++ has
shared_ptr
which allows the creation of another shared_ptr
pointing at a field of
a shared_ptr
's pointee. This is possible, because shared_ptr
is made up of
two pointers, one pointing to the data and another pointing at the ref count.
While this is not possible to add to Arc
without introducing a new field, it
could be possible to add another Arc
pointer that allowed field projections.
See this section for more, as this is out of this RFC's
scope.
RFCs
Further discussion
Unresolved questions
Before merging
-
Is new syntax for the borrowing necessary (e.g.
&pin mut x.y
or&uninit mut x.y
)? -
how do we disambiguate field access when both the wrapper and the struct
have the same named field?
MaybeUninit
<Struct>.value
andStruct
also has.value
.
Before stabilization
- How can we enable users to leverage field projection? Maybe there should exist a public trait that can be implemented to allow this.
-
Should
union
s also be supported? -
How can
enum
andMaybeUninit
<T>
be made compatible?
Future possibilities
Arrays
Even more generalized projections e.g. slices: At the moment
exist, maybe there is room for generalization here as well.
Rc
<T>
and Arc
<T>
projections
While out of scope for this RFC, projections for Rc
<T>
and Arc
<T>
could be implemented by adding another field that points to the ref count.
This RFC is designed for low cost projections, modifying an atomic ref count is
too slow to let it happen without explicit opt-in by the programmer and as such
it would be better to implement it via a dedicated map
function.
There has been a discussion on zulip. It started as an effort to provide ergonomic pin-projections.
Here are some things that I want to improve before creating a pull request:
- extend the guide-level explanation with:
enum
PinnedDrop
Changelist:
Edit #1
- added chapter on `Arc`/`Rc`
Edit #2
- Removed `Ref[Mut]`
- clarified Pin and pin example
- added attribute implementation details
- projecting arc section
- specified projectable pointers
- elaborated on design rationale
- updated unresolved questions
Edit #3
- fixed task lists
- added field name disambiguation question