you can't safely convert from Pin<&mut T> to &mut T
Sorry, that was abusive notation. In the original post I used &'static mut T
to avoid this concern and then dropped it for simplicity. All posts are written with &'static mut T
in mind.
Line 6 is UB, but wouldn't be UB if not for line 5. However, the only unsafe code is the conversion on line 4, so the UB has to be attributed to the conversion. That's equivalent to saying that the conversion has the safety postcondition "don't access the returned reference after deallocating it".
I think this example is a pre-condition. The unsafe code violates the pre-condition.
/// Pre: ptr is non-null, aligned, and dereferenceable
/// Pre: *ptr is initialized and satisfies aliasing for &'a
unsafe fn as_ref_unchecked<'a>(ptr: *const i32) -> &'a i32;
// There is no lifetime that simultaneously satisfies the drop and println lines.
one example is Rc::get_mut_unchecked
This is not a (low- to high-level) conversion function per se. An Rc<T>
points to an RcBox<T>
, not a T
, so we don't just change the type, we also change the pointer value. But I don't think it really matter for the concern of examples where a post-condition extends a type. I also think in this case it's a pre-condition.
/// Pre: *this.as_ptr() satisfies aliasing for &'a mut
unsafe fn get_mut_unchecked<'a>(this: &'a mut Rc<T>) -> &'a mut T;
Similarly for RefCell::try_borrow_unguanded
.
Note that those examples don't restrict what you can do with the result, but what you can do with the input. While Pin::get_unchecked_mut
restricts what you can do with the result. It's not stated as a property of the input (in particular the correct version would say "the pointed content is actually not pinned" but that would be a different semantics and not solve the intent of why get_unchecked_mut
exists).
the UB would be caused immediately at the point of conversion
Yes, I'd argue that. (Note that it's not because UB happens there, that it's visible there at runtime.)
But that reasoning is only valid if lifetimes affect runtime semantics
I'm not sure what runtime semantics mean. Lifetimes affect semantics indeed, this is why we can say [&'a mut T] β [&'b mut T]
if 'a
outlives 'b
. If they did not affect semantics then those would be the same set of behaviors.
it would be too unpredictable and difficult to reason about for unsafe code authors
That's an interesting point. I'm curious to know more about this, because in my opinion, if we say that unsafe authors shouldn't need to reason about lifetime, then we essentially cancel the notion of lifetime which is one of the most powerful tool of Rust.
given an &mut str, you can call the unsafe as_bytes_mut to get an &mut [u8]
This is a very interesting example. I think this is a valid example that builds on the RustHorn idea to describe the end value when borrowing.
Let's look at its different properties:
- This is a high- to low-level conversion function.
- High- to low-level conversion functions may have post-condition refining their result to still allow round-tripping back to the high-level version (e.g. from
NonNull<T>
to*mut T
we need the post-condition that*mut T
is non-null otherwise we can't go back) but this doesn't make the function unsafe, it is just additional information. - High- to low-level conversion functions are usually safe. However here, the round-trip is automatic due to the temporary borrow. So we have to prove during the high- to low-level conversion that the low- to high-level conversion is valid (because it happens automatically at the end of the borrow). This is what the post-condition is doing.
- However, the post-condition still refines the type. Being valid UTF-8 at end of lifetime restricts the set of behaviors of
&mut [u8]
. So we are still refining the type.
/// Post: result must be valid UTF-8 at end of lifetime
unsafe fn as_bytes_mut(self: &mut str) -> &mut [u8];
// [&mut str].typ β [&mut [u8]].doc β [&mut [u8]].typ
Because you can always safely forget that the slice must be valid UTF-8 at end of lifetime, you can safely call functions taking &mut [u8]
.
I'll try to write a summary of the thread in the original post because my initial worry is solved by Ralf's blog post. We are now discussing a less important part of my worry (that I didn't expect existed in Rust, but I see why it's a pragmatic choice, and my dream language is probably yet to come and will learn from Rust's experience, might be a future Rust edition).