Rust continues to top the charts as the most admired and desired language by developers, and in this post, we dive a little deeper into how (and why) Rust is stealing the hearts of developers around the world.
If you run Miri on your code (Tools -> Miri in the Playground), it actually seems to complain about UB. I’m not experienced enough with unsafe rust to translate that error message to something meaningful though.
Edit: Wait, that’s the while_here_it_isnt method. I’m clearly tired…
Until yesterday I wouldn’t have expected either to be sane.
But then I got the reply above, that aliasing pointers is fine. The playground link is how I interpreted that statement.
Edit: Lemmy decided to completely butcher my comment, so I’ve replaced all the ampersands with %. Sorry, this will look a bit funny.
You (and they) are right that aliasing pointers is fine. I was running Miri on your playground link, and it gave the expected results. I was just too tired to realize that it was saying your failure case (where you did multiple mutable aliasing with borrows) caused UB and that your success case (where you did multiple mutable aliasing with pointers) did not cause UB.
Generally speaking, the rules around aliasing only apply to borrows in Rust, from my understanding. Any code that creates two %mut borrows of the same value is immediate UB. Any code that could possibly cause that to happen using safe code is unsound. Since your method operates only on the raw pointers, no aliasing rules have been broken, however the compiler also can’t optimize around your code the same way it could had you used regular borrows (assuming it’s possible). At a lower level, this is reflected by the compiler telling LLVM that %mut T values (usually) are not aliased, and LLVM applies optimizations around that. (Note that UnsafeCell is a bit of a weird case, but is fundamental to how the other cell types work.)
This is actually why shared pointers like Rc and Arc only give you shared borrows (%) of the values contained in them, and why you’re required to implement some kind of interior mutability if you want to mutate the shared values. The shared pointer cannot guarantee that two borrows of the same value are not active at the same time, but does allow for shared ownership of it. The Cell/RefCell/Mutex/etc types verify that there is only one active %mut T (unique borrow) of the inner value at a time (or in Cell’s case even allows you to mutate without ever receiving a %mut T).
Note that while %T and %mut T are often referred to as “immutable” and “mutable” references, it’s probably more accurate to refer to them as “shared” and “unique” references. Mutability is not actually tied to whether you have a %T or a %mut T. This is trivially shown by looking at the Atomic* types, which only require a %self for their store operation.
@soulsource@anlumo dude your whole code is UB. A reference & means that the data behind it never changes while any reference exists, allowing multiple pointers to point at it at the same time (aliasing); whereas a mutable reference &mut means that the data behind may only be read or written by that pointer, i.e. multiple pointers (aliasing) can’t exist. The compiler uses this to optimize code and remove stuff that you promise never happens. Always use miri, and go read the nomicon.
Thanks for correcting my worldview, because after that playground behaved as it should if aliasing were allowed my worldview was kinda shattered. Oh, and I had completely forgotten that Playground has Miri built in.
I left something important out from my explanation. Your example still holds ownership of the data, so that’s where the rules are violated with those raw pointers. You have to use Box::into_raw or something similar to disassociate the data from the Rust compiler. Then you can alias it using raw pointers.
It seems I misunderstood something important here. I’d take that as proof that Unsafe Rust is rarely needed. 😜 A quick test on the Playground shows that indeed, using raw pointers does not yield the wrong result, while using references does: https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=96f80d43d71a73018f23705d74b7e21d
Conclusion: Unsafe Rust is not as difficult as I thought.
If you run Miri on your code (Tools -> Miri in the Playground), it actually seems to complain about UB. I’m not experienced enough with unsafe rust to translate that error message to something meaningful though.
Edit: Wait, that’s the
while_here_it_isnt
method. I’m clearly tired…Until yesterday I wouldn’t have expected either to be sane. But then I got the reply above, that aliasing pointers is fine. The playground link is how I interpreted that statement.
So, if my previous intuition was correct, how is https://discuss.tchncs.de/comment/2544085 to be interpreted?
Edit: Lemmy decided to completely butcher my comment, so I’ve replaced all the ampersands with
%
. Sorry, this will look a bit funny.You (and they) are right that aliasing pointers is fine. I was running Miri on your playground link, and it gave the expected results. I was just too tired to realize that it was saying your failure case (where you did multiple mutable aliasing with borrows) caused UB and that your success case (where you did multiple mutable aliasing with pointers) did not cause UB.
Generally speaking, the rules around aliasing only apply to borrows in Rust, from my understanding. Any code that creates two
%mut
borrows of the same value is immediate UB. Any code that could possibly cause that to happen using safe code is unsound. Since your method operates only on the raw pointers, no aliasing rules have been broken, however the compiler also can’t optimize around your code the same way it could had you used regular borrows (assuming it’s possible). At a lower level, this is reflected by the compiler telling LLVM that%mut T
values (usually) are not aliased, and LLVM applies optimizations around that. (Note thatUnsafeCell
is a bit of a weird case, but is fundamental to how the other cell types work.)This is actually why shared pointers like
Rc
andArc
only give you shared borrows (%
) of the values contained in them, and why you’re required to implement some kind of interior mutability if you want to mutate the shared values. The shared pointer cannot guarantee that two borrows of the same value are not active at the same time, but does allow for shared ownership of it. TheCell
/RefCell
/Mutex
/etc types verify that there is only one active%mut T
(unique borrow) of the inner value at a time (or inCell
’s case even allows you to mutate without ever receiving a%mut T
).Note that while
%T
and%mut T
are often referred to as “immutable” and “mutable” references, it’s probably more accurate to refer to them as “shared” and “unique” references. Mutability is not actually tied to whether you have a%T
or a%mut T
. This is trivially shown by looking at theAtomic*
types, which only require a%self
for theirstore
operation.@soulsource @anlumo dude your whole code is UB. A reference
&
means that the data behind it never changes while any reference exists, allowing multiple pointers to point at it at the same time (aliasing); whereas a mutable reference&mut
means that the data behind may only be read or written by that pointer, i.e. multiple pointers (aliasing) can’t exist. The compiler uses this to optimize code and remove stuff that you promise never happens. Always use miri, and go read the nomicon.That was how I thought it works until yesterday. And Miri seems to confirm what I thought.
But then there was this comment, that suggested otherwise: https://discuss.tchncs.de/comment/2544085
Thanks for correcting my worldview, because after that playground behaved as it should if aliasing were allowed my worldview was kinda shattered. Oh, and I had completely forgotten that Playground has Miri built in.
@soulsource @anlumo useful links:
https://predr.ag/blog/falsehoods-programmers-believe-about-undefined-behavior/
https://doc.rust-lang.org/nomicon/
https://rust-unofficial.github.io/too-many-lists/
https://github.com/rust-lang/miri#using-miri
I left something important out from my explanation. Your example still holds ownership of the data, so that’s where the rules are violated with those raw pointers. You have to use
Box::into_raw
or something similar to disassociate the data from the Rust compiler. Then you can alias it using raw pointers.