Pin layout guarantees?

Is the layout/repr/ABI of Pin<P> guaranteed to be equal to P? It is marked #[repr(transparent)] but I see no mention of layout in the docs. Is #[repr(transparent)] a stable guarantee (even without a mention in the type's documentation)?

No.

It is not in general, but in the specific case of Pin I believe it is?

Unless it has been documented somewhere, then that's not the case. A quick ctrlF for "layout" on the docs for both std::pin and std::pin::Pin shows nothing. Likewise for the RFC that proposed it.

(NOT A CONTRIBUTION)

I cannot imagine a scenario in which the layout of Pin would change, and probably a documentation change to explicitly guarantee it would be accepted.

The #repr information is not in the prose of the documentation, but it is absolutely rendered by rustdoc. I'd personally assume that would be enough for Rust's backwards-compat rules to come into play? Fields and methods don't need to be explicitly mentioned in prose either to be considered stable.

If the intent is that such annotations not be considered binding on the team, then rustdoc should be changed to not emit them

2 Likes

#[repr(transparent)] can only be relied upon by users if the layout of the inner type (for which the outer type has transparent layout) is documented somewhere, eg by the field being publicly visible or otherwise in the prose.

That makes sense, but is decidedly unhelpful (and even misleading) on the part of rustdoc. I guess I'm the latest victim of Hide `#[repr(transparent)]` where the field is non-public · Issue #90435 · rust-lang/rust · GitHub

EDIT: To expand a bit, I presume the following is a totally legal pin implementation:

struct NotSoTransparent<P> {
    _RandomPadding: [u8; 42],
    pointer: P
}

#[repr(transparent)]
struct Pin<P> {
    inner: NotSoTransparent<P>
}

Beat me to responding that I had already created an issue about it.

For clarity, I have no reason to believe that the layout of Pin will ever change, and it should be explicitly documented.

If I recall correctly we have some kind of (temporary solution / hack) language feature that Pin<&mut T> gets more lenient treatment for aliasing concerns, or something along those lines, in order to ensure soundness of self-referential Futures for async functions and blocks

I don't have any links to where this is mentioned off the top of my head, but one might want to look into that in order to determine whether in that context, the act of transmuting between Pin<&mut T> and &mut T could have any undesirable effects, as opposed to using the normal ways of conversion. Quite possibly this is of no concern at all, but with pointers and transmution, for me, always concerns like provinence or e. g. how it's AFAIR problematic to transmute between *const T and usize (and one ought to use as casts instead), even though their layout is the same.

Also, in general, whilst one side of the argument goes "I don't see any reason why Pin should ever change its layout", on the other side here's me wondering "what is the actual practical use-case for transmuting between P and Pin<P>?" Maybe it's just a lack of creativity, but I'm really a bit curious.

1 Like

Current understanding is that it can't be Pin that is special, it has to be the T that is special. This is due to the map_unchecked_mut and as_mut APIs necessarily going through &mut T while manipulating pinned references. The concept of whether a reference is pinned is purely one of API safety/soundness and not something which the language operational semantics care about.

(This is a summary of my understanding but isn't a report of an actual opsem decision.)

This magical quality is currently that types which are not Unpin are exempt from &mut uniqueness, but this is only 100% guaranteed to work for async futures. We may in the future change this to use a model closer to UnsafeCell/Freeze (tentatively called UnsafeMutCell/FreezeMut). (UnsafeCell might also change to work like Unpin in the opsem and lose field level granularity; it depends on a number of factors.)

Current draft behavior is that reading a pointer as usize (a transmute) soundly just discards the provenance, and reading usize as a pointer gives you a pointer with no provenance, which is identical to ptr::invalid. These are still open to change, but I think this is reasonably likely, and we don't have meaningful reason to make transmutes immediate UB.

A by value transmute is pretty cursed and you should use as or one of the strict provenance functions instead, but when part of a larger struct and/or behind indirection it can make some sense, at least in the -> usize direction.

The big one is the implicit transmute of using Pin<&mut T> in FFI, matching up with T* in C(++). I agree #[repr(transparent)] isn't sufficient on its own to actually imply a semver guarantee of layout, but I do believe Pin is as good as layout stable and it's merely a matter of documenting that fact properly.

5 Likes

Ah, the classic mixup... what I had in mind might have indeed been about Unpin having special effects, not about Pin.

The layout is now documented: Document `Pin` memory layout by Jules-Bertholet · Pull Request #111711 · rust-lang/rust · GitHub

2 Likes