Skip to content

RFC: Aligned trait #3319

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
189 changes: 189 additions & 0 deletions text/3319-aligned-trait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
- Feature Name: `aligned`
- Start Date: 2022-09-24
- RFC PR: [rust-lang/rfcs#3319](https://github.com/rust-lang/rfcs/pull/3319)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary

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](https://github.com/rust-lang/rust/blob/a76ec181fba25f9fe64999ec2ae84bdc393560f2/compiler/rustc_data_structures/src/aligned.rs#L22-L25)
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](https://github.com/rust-lang/rust/blob/a76ec181fba25f9fe64999ec2ae84bdc393560f2/compiler/rustc_middle/src/ty/list.rs#L222-L232),
it needs to use a custom trait, along with potentially error-prone `unsafe`
impls.

## Case study: `unsized-vec`

The [`unsized-vec` crate](https://docs.rs/unsized-vec/0.0.2-alpha.7/unsized_vec/)
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:

```text
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:

1. Retrieve the 2nd metadata entry from the metadata allocation (value is 3);
2. Retrieve the 2-1=1st offset entry from the metadata allocation (value is 2);
3. Offset the address of the main allocation (`0xDEADBEEF`) by the result of
step 2 to get the pointer to the element (address `0xDEADBEF1`);
4. 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>`:

```text
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 `Sized` bounds.
- May make certain object safety diagnostics more confusing, as they will now
refer to the new, lesser-known `Aligned` trait instead of `Sized`.

# 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.
- `?Aligned` could 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 `Sized` but not `Aligned`. However,
I don't know of such, and allowing it would likely cause
backward-compatibility issues.

# Prior art

In libraries:

- [`rustc`](https://github.com/rust-lang/rust/blob/f9a6b71580cd53dd4491d9bb6400f7ee841d9c22/compiler/rustc_data_structures/src/aligned.rs#L22)
- [`unsized-vec`](https://github.com/Jules-Bertholet/unsized-vec/blob/278befae4c08db42ff77461e9d7ce30eccf0c5bc/src/marker.rs#L16)

# Unresolved questions

- Should `Aligned` be `#[fundamental]`? (`Sized` is.)

# Future possibilities

- Relaxing `NonNull::<T>::dangling()`'s trait bound from `T: Sized` to
`T: ?Sized + Aligned + Pointee<Metadata: ~const Default>` may be desirable
once the necessary library and language features are stabilized.
- `extern type`s may want to be able to implement `Aligned`.
- In a future edition, `?Sized` could be replaced with `?Aligned`, with `?Sized`
then meaning "opt out of `Sized` bound only, not `Aligned`."
- Certain `Self: Sized` bounds in the standard library could be relaxed to
`Self: Aligned`. However, this might cause backward-compatibility issues.
- [IRLO topic](https://internals.rust-lang.org/t/removing-self-sized-and-backward-compatibility/17456)
on how the issues could be addressed.
- There has been discussion about adding other traits into the `Sized`
hierarchy, like `DynSized`. If both `Aligned` and these other traits are
integrated into Rust, their relative positions in the trait hierarchy will
need to be determined.