Description
#81513 introduces a ptr::Pointee
type. This interface lacks a couple key bits of info, two key bits critical for allocators and stuff like ThinBox
.
Every pointee in practice has to have some sort of defined layout, and as pointers exist in a finite space, layout is also guaranteed to be finite. rust-lang/rfcs#3536 offers a partial solution for sizes, but doesn't address the two cases where alignment isn't statically known: the dynamic alignment of dyn Trait
and the undefined alignment of opaque types.
So, ptr::Pointee
needs to expose a way to get this information, as it was initially intended to. There's a few constraints:
- It must be type-safe as much as possible. And what can't be checked at the type level must be easily checked in full by Miri.
- Where things can be inferred or generated, it must be inferred or generated. Otherwise, it'd disrupt a lot of code.
- It of course obviously needs to work within the confines of Tracking Issue for pointer metadata APIs #81513. This goes without saying.
The simplest way I can think of is unfortunately still somewhat involved, and I'm not sure I can reduce it any further:
-
Add a required
fn layout(&self) -> alloc::Layout
method toptr::Pointee
.Using
Layout
and&self
simplifies the interface a lot. -
Add a
marker::Aligned
trait similar tomarker::Sized
, representing that the type has a statically defined alignment. Almost everything auto-implements this, butdyn Trait
and opaque types will not.Sized
will be modified to subtype this. The only way to declare an unknown alignment is to use anextern
type, a type of DST, and so it's not possible to both make the size statically determinable and leave the alignment unknown.Like its counterpart
Sized
,Aligned
must be implemented to read something by value, and generic parameters implicitly add anAligned
constraint unless you explicitly add?Aligned
. Unlike it,Aligned
may be manually implemented forextern
types via a#[repr(align(N))]
attribute.Why an attribute? Makes things way easier for the compiler.
-
Make
Pointee
anunsafe
trait. The lack of safety is because it's defining the memory safety boundaries.The
layout
method is safe to call, but the method itself must ensure that:- The returned layout's size holds a full
T
. - The returned layout's align returns the actual runtime alignment of the
&self
pointer.
- The returned layout's size holds a full
-
Auto-implement
Pointee
for every type that either implementsAligned
or has a final field that implementsPointee
.If the final field does not implement
Aligned
but does implementPointee
, the auto-generated implementation returns essentiallystatic_prefix.extend(final_field.layout()).unwrap().0
.Pointee
is not auto-implementedextern
types. Their layout is undefined, and only the programmer will know how it should be laid out. It also makes for a nice escape hatch. -
Generics will assume
Pointee
to be implemented by default. It'll be likeSized
rather thanUnpin
in that regard.This avoids breaking things like
struct Foo<T>(Arc<T>)
.
Here's how it'd look for the bulk of this at the type level:
use core::alloc::Layout;
use core::ptr::metadata;
// `core::marker::Aligned`
#[lang = "aligned"]
pub trait Aligned {
// Empty
}
// `core::marker::Sized`
#[lang = "sized"]
pub trait Sized: Aligned {
// Empty
}
// `core::ptr::Pointee`
#[lang = "pointee"]
pub unsafe trait Pointee {
type Metadata: Copy + Send + Sync + Ord + core::hash::Hash + Unpin;
fn layout(&self) -> Layout;
}
unsafe impl<T: Sized + Aligned> Pointee for T {
type Metadata = ();
const fn layout(&self) -> Layout {
Layout::new::<T>()
}
}
// Generated for every `trait Trait`
unsafe impl Pointee for dyn Trait {
type Metadata = DynMetadata<Self>;
fn layout(&self) -> Layout {
metadata(self).layout()
}
}
unsafe impl<T: Sized + Aligned> Pointee for [T] {
type Metadata = usize;
const fn layout(&self) -> Layout {
let size = core::mem::size_of::<T>();
let align = core::mem::align_of::<T>();
let len = self.len();
unsafe {
Layout::from_size_align_unchecked(len.unchecked_mul(size), align)
}
}
}
unsafe impl Pointee for str {
type Metadata = usize;
const fn layout(&self) -> Layout {
let len = self.len();
unsafe {
Layout::from_size_align_unchecked(len, 1)
}
}
}
// `core::ffi::CStr`
extern "C" {
#[repr(align(1))]
pub type CStr;
}
unsafe impl Pointee for CStr {
type Metadata = ();
fn layout(&self) -> Layout {
let len = self.len();
unsafe {
Layout::from_size_align_unchecked(len.unchecked_add(1), 1)
}
}
}
// `Layout::for_value` should just delegate to `Pointee::layout`
impl Layout {
fn for_value<T: ?Sized>(value: &T) -> Layout {
<T as core::ptr::Pointee>::layout(value)
}
}
// `mem::size_of_val` and `mem::align_of_val` no longer need intrinsics
pub fn size_of_val<T: ?Sized + ?Aligned>(value: &T) -> usize {
<T as core::ptr::Pointee>::layout(value).size()
}
pub fn align_of_val<T: ?Sized + ?Aligned>(value: &T) -> usize {
<T as core::ptr::Pointee>::layout(value).align()
}
There's a couple other benefits this provides:
- It removes the need for two compiler intrinsics:
size_of_val
andalign_of_val
. Their implementations are of course in the above code block. - Miri can check custom
layout
s against the struct's backing memory on construct and on every pointer cast to the type, ensuring the referenced memory region is always valid and that the pointer in question is correctly aligned.