Add an Aligned marker trait to core::marker, and the prelude, as a
supertrait of the Sized trait. Aligned is implemented for all types with an
alignment determined at compile time. This includes all Sized types, as well
as slices and records containing them. Relax core::mem::align_of<T>()'s trait
bound from T: Sized to T: ?Sized + Aligned.
Motivation
Some data structures and containers can store unsized types only if their
alignment can be known at compile-time. Additionally, compile-time known
alignment can enable more efficient algorithms and additional APIs. A built-in
Aligned trait allows Rust code to implement APIs that that are fully usable
with all aligned types.
In addition, this RFC allows implementing certain object-safe traits for slices, in a more complete fashion than was possible before.
Case study: rustc Aligned trait
In rustc, a manually-implemented version
of the Aligned trait is used to support pointer tagging. A pointer to a type
with known alignment has log2(alignment) low bits available for a tag. Because
rustc uses pointer tagging with unsized types, including custom DSTs,
it needs to use a custom trait, along with potentially error-prone unsafe
impls.
Case study: unsized-vec
The unsized-vec crate
provides an analogue of the standard library Vec<T> that permits T: ?Sized.
The layout of the UnsizedVec<T> differs depending on whether T's size or
alignment can be known at compile-time:
T: Sized
If T: Sized, UnsizedVec<T> is a thin wrapper around alloc::vec::Vec<T>.
T: ?Sized, alignment known at compile-time
In this case, UnsizedVec<T> contains two allocations. One allocation contains
the values of the vector, laid out end-to-end, aligned to T's alignment. The
other allocation is an alloc::vec::Vec, of
(ptr_metadata, offset_of_end_of_element_from_start_of_allocation) pairs (one
for each element of the UnsizedVec). For example, an UnsizedVec<[u8]> might
look like this:
Layout of `unsize_vec![[1, 2], [3, 4, 5], [], [6]]`
---------------------------------------------------
Main allocation (1 block = 1 byte)
⮦Address 0xDEADBEEF
┌──────┬──────┬──────┬──────┬──────╥──────┐
│ 0x01 0x02 │ 0x03 0x04 0x05 ║ 0x06 │
└──────┴──────┴──────┴──────┴──────╨──────┘
Metadata allocation (1 block = 1 word, "meta" = pointer metadata, "ofst" = offset)
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│meta 2 ofst 2│meta 3 ofst 5│meta 0 ofst 5│meta 1 ofst 6│
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
To retrieve a reference to the element at index i of the UnsizedVec, we
first index into the pairs Vec: once at index i to get the pointer metadata,
and once at index i - 1 to get the offset of the start of element (if i = 0,
this offset is just 0). We then offset the pointer to the main allocation by
the offset we just retrieved, and reconstruct the fat pointer to the
UnsizedVec element from that result + the pointer metadata. For example, to
retrieve element 1 (0-indexed) of the vec above, we would:
- Retrieve the 2nd metadata entry from the metadata allocation (value is 3);
- Retrieve the 2-1=1st offset entry from the metadata allocation (value is 2);
- Offset the address of the main allocation (
0xDEADBEEF) by the result of step 2 to get the pointer to the element (address0xDEADBEF1); - Combine that with the metadata from step 1 to construct the full fat
pointer
(pointer = 0xDEADBEF1, metadata = 3).
T: ?Sized, aligment known only at runtime
When T's alignment is not known at compile-time, UnsizedVec<T>'s elements
can't just be placed end-to-end, as that would lead to them being improperly
aligned. To account for the varing alignments, we keep track of the largest
required alignment out of all the elements in the UnsizedVec, and ensure every
that every element is padded to that maximum. For example, consider this
UnsizedVec<dyn Debug>:
Layout of `unsize_vec![3_u32, "hello", 42_u128]`
---------------------------------------------------
Maximum alignment: 16 bytes
Main allocation (1 block = 4 bytes)
⮦Address 0xDEADBEEF
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ 3u32 │ padding │ 64-bit ptr │ padding │ 42_u128 │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
Metadata allocation (1 block = 1 word, "vp" = vtable pointer, ofst = "offset")
┌──────┬──────┬──────┬──────┬──────┬──────┐
│vp u32 ofst16│vp&str ofst32│vpu128 ofst48│
└──────┴──────┴──────┴──────┴──────┴──────┘
Compared to the unsized+aligned case, we must add introduce an extra field to
our UnsizedVec in order to track alignment, and also need additional code to
manage the padding. We would prefer to pay that cost only when truly necessary;
only an Aligned trait provided by the compiler can make that possible.
Guide-level explanation
Aligned is a marker trait defined in core::marker, and re-exported in the
prelude. It's automatically implemented for all types with an alignment
determined at compile time. This includes all Sized types (Aligned is a
supertrait of Sized), as well as slices and records containing them. Trait
objects are not Aligned.
You can't implement Aligned yourself.
To get the alignment of a type that implements Aligned, call
core::mem::align_of<T>().
Implied Sized bounds also imply Aligned, because Aligned is a supertrait
of Sized. To bound a type parameter by Aligned only, write
?Sized + Aligned.
Reference-level explanation
Aligned is not object-safe. Trait methods bounded by Self: Aligned can't be
called from a vtable, but don't affect the object safety of the trait as a
whole, just like Self: Sized currently. Relaxing Self: Sized bounds to
Self: Aligned allows implementing those methods for more Self types, while
preserving the trait's object safety.
core::mem::offset_of! supports any Aligned field.
Drawbacks
- Slightly complicates situation around implied
Sizedbounds. - May make certain object safety diagnostics more confusing, as they will now
refer to the new, lesser-known
Alignedtrait instead ofSized.
Rationale and alternatives
core::mem::align_of<T>()for slices could be implemented with a library. However, a library would be unable to support records that contain a slice as the last field. Also, relaxing the trait dyn safety requirements can only be done with a language feature.?Alignedcould be accepted as new syntax, equivalent to?Sized. However, I don't think it's worth it to have two ways to spell the exact same concept in the same edition.- There may be a use-case for types that are
Sizedbut notAligned. However, I don't know of such, and allowing it would likely cause backward-compatibility issues.
Prior art
In libraries:
Unresolved questions
- Should
Alignedbe#[fundamental]? (Sizedis.)
Future possibilities
- Relaxing
NonNull::<T>::dangling()'s trait bound fromT: SizedtoT: ?Sized + Aligned + Pointee<Metadata: ~const Default>may be desirable once the necessary library and language features are stabilized. extern types may want to be able to implementAligned.- In a future edition,
?Sizedcould be replaced with?Aligned, with?Sizedthen meaning "opt out ofSizedbound only, notAligned." - Certain
Self: Sizedbounds in the standard library could be relaxed toSelf: Aligned. However, this might cause backward-compatibility issues.- IRLO topic on how the issues could be addressed.
- There has been discussion about adding other traits into the
Sizedhierarchy, likeDynSized. If bothAlignedand these other traits are integrated into Rust, their relative positions in the trait hierarchy will need to be determined.