Skip to content

ACP : Enshrine the fact that slices are structurally pinning through safe accessors #607

Open
@Morgane55440

Description

@Morgane55440

Proposal

Problem statement

Currently, all APIs that are exposed by the standard library regarding pinning are for individual items, mainly through Box but also Rc and Arc.

according to the pin documentation, a type is structurally pinning if it being pinned if implies that its fields are pinned.

while it would seem obvious to most people that slices are structurally pinnig, and people do actually rely on this behavior, there are such no guarantees yet (at least that i could find that it is the case).
Moreover, because there are no safe API to access the members of a pinned slice, developers have to rely on unsafe code to do work with pinned slices, putting them at risk of making mistakes and triggering UB.

Subslicing and the drop guarantees of pinning

From the moment a value is pinned by constructing a Pinning pointer to it, that value must remain, valid, at that same address in memory, until its drop handler is called.

  • pin module documentation

whit types other than slices, this is not an issue at all, even when pin projection comes into play, as the outer structure being dropped in place means the inner fields will be dropped in place too (if the projection is implemented correctly)

but, going by the strictest, most conservative interpretation of this guarantee, creating a pinned reference to a subslice of a pinned slice is breaks this guarantee because the drop handler to that specific subslice will never be called.

of course, while this is a theoretical issue, in practice, it shouldn't really matter much. indeed, slices do not implement Drop, and simply drop their elements as part of normal drop glue.

therefore it is my opinion (and something that would need to be agreed upon to implement pinned subslicing) that:

if a pinned slice long completely contains a pinned slice short, then dropping long does fulfill the Drop guarantee of short

this is a relaxation of the strictest possible interpretation of the guarantee, and thus does not prevent any further relaxations.
i could think of 2 such relaxations :
a slightly more relaxed one :
the Drop guarantee of a slice is fulfilled if all of it's elements are dropped in order (code accessing the yet undropped elements may be run between drops)
and the most relaxed one :
the Drop guarantee of a slice is fulfilled if all of it's elements are dropped. there are no order guarantee and any undropped element may be accessed as long as they aren't dropped

i would like to note that while it is of course not sound to ever have a reference to a half-dropped slice, this is not just about the lifetime of that reference slice itself, but also everything that happens until the grantees is fulfilled. one use case for the last one would be to allow Pin<Vec<T>> to exist and to dynamically shorten by dropping elements at the end.

Motivating examples or use cases

the following code, was first created in futures-util 6 years ago, and has been copied in a few other crates. it is the most common way these are dealt with.

fn iter_pin_mut<T>(slice: Pin<&mut [T]>) -> impl Iterator<Item = Pin<&mut T>> {
  // Safety: `std` _could_ make this unsound if it were to decide Pin's
  // invariants aren't required to transmit through slices. Otherwise this has
  // the same safety as a normal field pin projection.
  unsafe { slice.get_unchecked_mut() }
      .iter_mut()
      .map(|t| unsafe { Pin::new_unchecked(t) })
}

as a general rule, if we have many elements of the same type that need to be polled (futures or generators), then having the ability to put them and use that slice in a slice is pretty useful

Solution sketch

iterators

i can't think of any issues with these or alternative implementations

impl<'a, T> IntoIterator for Pin<&'a [T]> {
    type Item = Pin<&'a T>;
    type IntoIter = PinnedSliceIter<'a, T>;
}

impl<'a, T> IntoIterator for Pin<&'a mut [T]> {
    type Item = Pin<&'a mut T>;
    type IntoIter = PinnedSliceIterMut<'a, T>;
}

indexing

basic solution that does not involve subslicing :

impl<T> Pin[T] {
      pub fn index_pinned(self : Pin<&Self>, index: usize) -> Option<Pin<& T>>;

      pub fn index_pinned_mut(self : Pin<&mut Self>, index: usize) -> Option<Pin<&mut T>>;
}

with subslicing :

impl<T> [T] {
      pub fn index_pinned<I>(self : Pin<&Self>, index: I) -> Option<Pin<&<I as SliceIndex<[T]>>::Output>>;


     pub const fn split_at(self : Pin<&Self>, mid: usize) -> (Pin<&Self>, Pin<&Self>);

      pub fn index_pinned_mut<I>(Pin<&mut Self>, index: I) -> Option<Pin<&mut<I as SliceIndex<[T]>>::Output>>;

     pub const fn split_at_mut(self : Pin<&mut Self>, mid: usize) -> (Pin<&mut Self>, Pin<&mut Self>);

}

Alternatives

there is of course the question of whether subslices are considered sound to have. (most likely yes). this influences what can or cannot be implemented.

if subslices are accepted, then there is also the question of the drop order guarantee. elements of a slice are guaranteed to drop in order from start to end when that slice is dropped. does that guarantee translate to any pinned slice that is ever exposed ? this would make things like dropping the end first and the start second unsound, which i do believe are things are things that could potentially have a use.

if not even the simple Iterator + Indexing is accepted, here are alternatives i could think of :

  • simply add clear documentation that slices are structurally pinned, but refrain from adding any safe accessors
  • do nothing
  • decide that slices are not structurally pinning(probably a terrible idea),with the option to implement Unpin for all [T]

Links and related work

discussion on the forum :
https://users.rust-lang.org/t/working-with-pinned-slices-are-there-any-structurally-pinning-vec-like-collection-types/50634

discussion on zulip on the basic idea and potential caveats :
https://rust-lang.zulipchat.com/#narrow/channel/219381-t-libs/topic/Question.20about.20.22collection.22.20pinning.20in.20the.20standard.20library/with/524301245

a PR promoting roughly the same idea that was closed de to inactivity :
rust-lang/rust#78370

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions