This RFC guarantees the in-memory representation of slice and str references.
Specifically, &[T] and &mut [T] are guaranteed to have the same layout as:
The layout of &str is the same as that of &[u8], and the layout of
&mut str is the same as that of &mut [u8].
Motivation
This RFC allows non-Rust (e.g. C or C++) code to read from or write to existing slices and to declare slice fields or locals.
For example, guaranteeing the representation of slice references allows
non-Rust code to read from the data or len fields of string in the type
below without intermediate FFI calls into Rust:
Note: prior to this RFC, the type above is not even properly repr(C) since the
size and alignment of slices were not guaranteed. However, the Rust compiler
accepts the repr(C) declaration above without warning.
Guide-level explanation
Slice references are represented with a pointer and length pair. Their in-memory
layout is the same as a #[repr(C)] struct like the following:
The precise ABI of slice references is not guaranteed, so &[T] may not be
passed by-value or returned by-value from an extern "C" fn.
The validity requirements for the in-memory representation of slice references
are the same as those documented on std::slice::from_raw_parts for shared slice references, and
those documented on std::slice::from_raw_parts_mut
for mutable slice references.
Namely:
-
datamust be non-null, valid for reads (for shared references) or writes (for mutable references) forlen * mem::size_of::<T>()many bytes, and it must be properly aligned. This means in particular:- The entire memory range of this slice must be contained within a single allocated object! Slices can never span across multiple allocated objects.
datamust be non-null and aligned even for zero-length slices or slices of ZSTs. One reason for this is that enum layout optimizations may rely on references (including slices of any length) being aligned and non-null to distinguish them from other data. You can obtain a pointer that is usable asdatafor zero-length slices using [NonNull::dangling()].
-
datamust point tolenconsecutive properly initialized values of typeT. -
The total size
len * mem::size_of::<T>()of the slice must be no larger thanisize::MAX, and adding that size todatamust not "wrap around" the address space. See the safety documentation of [pointer::offset].
str
The layout of &str is the same as that of &[u8], and the layout of
&mut str is the same as that of &mut [u8]. More generally, str behaves like
#[repr(transparent)] struct str([u8]);. Safe Rust functions may assume that
str holds valid UTF8, but it is not immediate undefined-behavior to store
non-UTF8 data in str.
Pointers
Raw pointers to slices such as *const [T] or *mut str use the same layout
as slice references, but do not necessarily point to anything.
Drawbacks
Zero-sized types
One could imagine representing &[T] as only len for zero-sized T.
This proposal would preclude that choice in favor of a standard representation
for slices regardless of the underlying type.
Alternatively, we could choose to guarantee that the data pointer is present if
and only if size_of::<T> != 0. This has the possibility of breaking exising
code which smuggles pointers through the data value in from_raw_parts /
into_raw_parts.
Uninhabited types
Similarly, we could be extra tricky and make &[!] or other &[Uninhabited]
types into a ZST since the slice can only ever be length zero.
If we want to maintain the pointer field, we could also make &[!] just a
pointer since we know the length can only be zero.
Either option may offer modest performance benefits for highly generic code which happens to create empty slices of uninhabited types, but this is unlikely to be worth the cost of maintaining a special case.
Compatibility with C++ std::span
The largest drawback of this layout and set of validity requirements is that it
may preclude &[T] from being representationally equivalent to C++'s
std::span<T, std::dynamic_extent>.
-
std::spandoes not currently guarantee its layout. In practice, pointer + length is the common representation. This is even observable usingis_layout_compatibleon MSVC, though not on GCC nor on Clang. Future changes to guarantee a different layout in the C++ standard (unlikely due to MSVC ABI stabilitiy requirements) could preclude matching the layout with&[T]. -
Unlike Rust,
std::spanallows thedatapointer to benullptr. One possibile workaround for this would be to guarantee thatOption<&[T]>usesdata: std::ptr::null(), len: 0to represent theNonecase, makingstd::span<T>equivalent toOption<&[T]>for non-zero-sized types.Note that this is not currently the case. The compiler currenty represents
None::<&[u8]>asdata: std::ptr::null(), len: uninit(though this is not guaranteed). -
Rust uses a dangling pointer in the representation of zero-length slices. It's unclear whether C++ guarantees that a dangling pointer will remain unchanged when passed through
std::span. However, it does support dangling pointers during regular construction via the use ofstd::to_addressin the iterator constructors.
Note that C++ also does not support zero-sized types, so there is no naive way
to represent types like std::span<SomeZeroSizedRustType>.
Flexibility
Additionally, guaranteeing layout of Rust-native types limits the compiler's and standard library's ability to change and take advantage of new optimization opportunities.
Rationale and alternatives
-
We could avoid committing to a particular representation for slices.
-
We could try to guarantee layout compatibility with a particular target's
std::spanrepresentation, though without standardization this may be impossible. Multiple different C++ stdlib implementations may be used on the same platform and could potentially have different span representations. In practice, current span representations also use ptr+len pairs. -
We could avoid storing a data pointer for zero-sized types. This would result in a more compact representation but would mean that the representation of
&[T]is dependent on the type ofT. Additionally, this would break existing code which depends on storing data in the pointer of ZST slices.This would break popular crates such as bitvec (55 million downloads) and would result in strange behavior such as
std::ptr::slice_from_raw_parts(ptr, len).as_ptr()returning a different pointer from the one that was passed in.Types like
*const ()/&()are widely used to pass around pointers today. We cannot make them zero-sized, and it would be surprising to make a different choice for&[()].
Prior art
The layout in this RFC is already documented in the Unsafe Code Guildelines Reference.
Future possibilities
- Consider defining a separate Rust type which is repr-equivalent to the platform's
native
std::span<T, std::dynamic_extent>to allow for easier interoperability with C++ APIs. Unfortunately, the C++ standard does not guarantee the layout ofstd::span(though the representation may be known and fixed on a particular implementation, e.g. libc++/libstdc++/MSVC). Zero-sized types would also not be supported with a naive implementation of such a type.