An alternative to the current `Unpin` hack:`UnsafeAlias`

One way to allow self-referential structs would be a new type UnsafeAlias, which allows a pointer and a mutable reference to overlap. The proposed api would be:

impl<T:?Sized> UnsafeAlias<T>{
    pub fn new(value:T) where T:Sized ->Self
    pub fn as_ref(&self)-&T
    pub fn from_ref(reference:&T)->&Self
    pub fn from_mut(reference:&mut T)->&mut Self
    pub fn into_inner(self)->T where T:Sized

    ///Safety:The returned pointer is safe to read from but can cause undefined behaviour 
    ///if written to(unless the data written to is inside an `UnsafeCell`)
    pub fn get_ptr(&mut self)->*const T
}

impl<T:?Sized> !Unpin for UnsafeAlias<T>{} 

This would allow self-referential structs and async functions while still allowing optimisations on shared references. On an llvm level, the only change would be removing the noalias annotation on mutable references pointing to a type containing a UnsafeAlias ;Shared references can remain noalias readonly(assuming the type does not contain a UnsafeCell).

The only change to Tree Borrows that would be needed to accommodate this would be adding a check to the transition from active to a frozen and leaving the reference in the active state if the affected byte is in an UnsafeAlias.

Example of a self referential struct using UnsafeAlias

Another example of code that would work

2 Likes

What is the intended/desired variance on T?

Probably covariance, because it is impossible for a downstream type to make it covariant if it is invariant.

1 Like

Would it make legal (that is, non-UB) to actually perform read and writes from both &mut UnsafeAlias<T> and *mut UnsafeAlias<T> or this only prevents the insta UB that happens when creating the alias?

(NOT A CONTRIBUTION)

You're proposing this in addition to Pin and Unpin, to replace the rules currently used for noalias, without changing the library-soundness aspects of Pin and Unpin? Presumably generated async function bodies would be wrapped in this (or treated as if they were by the compiler)?

Would this have a negative impl of Unpin? it seems like it should.

And because of this, anyone currently relying on the pin guarantees (like tokio) would now be unsound, right? They would need to wrap their types in this? So it would be a technically breaking change for those users.

It would be legal to read, but not write, to the derived pointer(with the normal exception for UnsafeCell); The fact that get_ptr() returns a *const instead of a *mut is supposed to imply this.

I will add that.

The easiest way to phase this in would be to say that it is technically Undefined Behaviour, but not to optimise around it until the next edition (Technically &mut !Unpin should have noalias applied now, so tokio(and other crates that rely on this behaviour) are already unsound).

btw this looks unsound

How could it cause unsoundness?

hmm... we guess UnsafeAlias is like some sort of AlwaysShared...

so &mut T to &mut AlwaysShared<T> is like &mut T to &T?

the problem is once you create the self-references...

hmm so maybe it's not inherently unsound but you'd need to undo the self-references by the time you go back to having a &mut T. which sounds like a footgun.

(we still think we'd be better off getting selfref into std...)

This sounds very similar to my own prior proposal. Would be good to see a comparison.

Pin guarantees in general are unaffected. This is about self-referential generators, which none of the Pin RFCs ever specified how to define.

What would be unsound is defining your own self-referential generators. We never stably guaranteed that you could even do that; the only stable way we expose is to write an async fn and those will be fixed by adding UnsafeCellMut/UnsafeAlias to their type in a suitable manner.

So, we might be able to justify this de jure. But more relevantly: how big is this problem? How many manual implementations of self-referential futures (not using async fn) are there out there that would be rendered unsound by this?

1 Like

there's selfref. and we use it. a lot. arguably too much.

but it's an abstraction, so we can easily update it.

1 Like

(NOT A CONTRIBUTION)

Tokio currently uses the pinning guarantee to implement many of its synchronization primitives using an intrusive linked list.

Oh, I think I see what you mean. Whether such code is rendered unsound heavily depends on the details of the code and the aliasing model (Stacked Borrows vs Tree Borrows).

I don't understand the need for this function:

pub fn get_ptr(&mut self) -> *const T

wouldn't this be equivalent to the following?

UnsafeAlias::as_ref(x) as *const T

Or is the pointer returned by get_ptr somehow magic?

I think the best solution would be to still leave the current !Unpin hack in place, but mark it as depreciated/don't document it at all. This means all existing code still works, but at some point in the future, we can remove the hack.