-
Notifications
You must be signed in to change notification settings - Fork 290
Add example of thinking about Send/Sync's soundness #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
a29fff0
8739b01
c3ba178
05a2df5
29f7139
2dd3566
357eab4
e542c32
75253ec
53661a5
9f9f909
a5da003
d060a51
09d4be2
484bbeb
d1c89a8
7a1be9d
33fa3de
4a910d8
047dc6b
6495701
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,7 +74,140 @@ of their pervasive use of raw pointers to manage allocations and complex ownersh | |
Similarly, most iterators into these collections are Send and Sync because they | ||
largely behave like an `&` or `&mut` into the collection. | ||
|
||
## Example | ||
|
||
[`Box`][box-doc] is implemented as it's own special intrinsic type by the | ||
compiler for [various reasons][box-is-special], but we can implement something | ||
with similar-ish behavior ourselves to see an example of when it is sound to | ||
implement Send and Sync. Let's call it a `Carton`. | ||
|
||
We start by writing code to take a value allocated on the stack and transfer it | ||
to the heap. | ||
|
||
```rust,ignore | ||
use std::mem::size_of; | ||
use std::ptr::NonNull; | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
struct Carton<T>(NonNull<T>); | ||
|
||
impl<T> Carton<T> { | ||
pub fn new(value: T) -> Self { | ||
// Allocate enough memory on the heap to store one T. | ||
let memptr = &mut null_mut() as *mut *mut T; | ||
unsafe { | ||
let ret = libc::posix_memalign( | ||
memptr as *mut *mut c_void, | ||
align_of::<T>(), | ||
size_of::<T>() | ||
); | ||
assert_eq!(ret, 0, "Failed to allocate or invalid alignment"); | ||
}; | ||
|
||
// NonNull is just a wrapper that enforces that the pointer isn't null. | ||
let mut ptr = unsafe { | ||
// Safety: memptr is dereferenceable because we created it from a | ||
// reference and have exclusive access. | ||
NonNull::new(*memptr) | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.expect("Guaranteed non-null if posix_memalign returns 0") | ||
}; | ||
|
||
// Move value from the stack to the location we allocated on the heap. | ||
unsafe { | ||
// Safety: If non-null, posix_memalign gives us a ptr that is valid | ||
// for writes and properly aligned. | ||
ptr.as_ptr().write(value); | ||
} | ||
|
||
Self(ptr) | ||
} | ||
} | ||
``` | ||
|
||
This isn't very useful, because once our users give us a value they have no way | ||
to access it. [`Box`][box-doc] implements [`Deref`][deref-doc] and | ||
[`DerefMut`][deref-mut-doc] so that you can access the inner value. Let's do | ||
that. | ||
|
||
```rust | ||
use std::ops::{Deref, DerefMut}; | ||
|
||
# struct Carton<T>(std::ptr::NonNull<T>); | ||
|
||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
impl<T> Deref for Carton<T> { | ||
type Target = T; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
unsafe { | ||
// Safety: The pointer is aligned, initialized, and dereferenceable | ||
// by the logic in [`Self::new`]. We require writers to borrow the | ||
// Carton, and the lifetime of the return value is elided to the | ||
// lifetime of the input. This means the borrow checker will | ||
// enforce that no one can mutate the contents of the Carton until | ||
// the reference returned is dropped. | ||
self.0.as_ref() | ||
} | ||
} | ||
} | ||
|
||
impl<T> DerefMut for Carton<T> { | ||
fn deref_mut(&mut self) -> &mut Self::Target { | ||
unsafe { | ||
// Safety: The pointer is aligned, initialized, and dereferenceable | ||
// by the logic in [`Self::new]. We require writers to mutably | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// borrow the Carton, and the lifetime of the return value is | ||
// elided to the lifetime of the input. This means the borrow | ||
// checker will enforce that no one else can access the contents | ||
// of the Carton until the mutable reference returned is dropped. | ||
self.0.as_mut() | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Finally, lets think about whether our `Carton` is Send and Sync. Something can | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
safely be Send unless it shares mutable state with something else without | ||
enforcing exclusive access to it. Each `Carton` has a unique pointer, so | ||
we're good. | ||
|
||
```rust | ||
# struct Carton<T>(std::ptr::NonNull<T>); | ||
// Safety: No one besides us has the raw pointer, so we can safely transfer the | ||
// Carton to another thread if T can be safely transferred. | ||
unsafe impl<T> Send for Carton<T> where T: Send {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now A nice example where this does not happen is with a
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a really good explanation. I tweaked it a little and added it to the page (a22d055). |
||
``` | ||
|
||
What about Sync? For `Carton` to be Sync we have to enforce that you can't | ||
write to something stored in a `&Carton` while that same something could be read | ||
or written to from another `&Carton`. Since you need an `&mut Carton` to | ||
write to the pointer, and the borrow checker enforces that mutable | ||
references must be exclusive, there are no soundness issues making `Carton` | ||
sync either. | ||
|
||
```rust | ||
# struct Carton<T>(std::ptr::NonNull<T>); | ||
// Safety: Our implementation of DerefMut requires writers to mutably borrow the | ||
// Carton, so the borrow checker will only let us have references to the Carton | ||
// on multiple threads if no one has a mutable reference to the Carton. This | ||
// means we are Sync if T is Sync. | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
unsafe impl<T> Sync for Carton<T> where T: Sync {} | ||
``` | ||
|
||
When we assert our type is Send and Sync we need to enforce that every | ||
contained type is Send and Sync. When writing custom types that behave like | ||
standard library types we can assert that we have the same requirements. | ||
For example, the following code asserts that a Carton is Send if the same | ||
sort of Box would be Send, which in this case is the same as saying T is Send. | ||
|
||
```rust | ||
# struct Carton<T>(std::ptr::NonNull<T>); | ||
unsafe impl<T> Send for Carton<T> where Box<T>: Send {} | ||
dzfranklin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
TODO: better explain what can or can't be Send or Sync. Sufficient to appeal | ||
only to data races? | ||
|
||
[unsafe traits]: safe-unsafe-meaning.html | ||
[box-doc]: https://doc.rust-lang.org/std/boxed/struct.Box.html | ||
[box-is-special]: https://manishearth.github.io/blog/2017/01/10/rust-tidbits-box-is-special/ | ||
[deref-doc]: https://doc.rust-lang.org/core/ops/trait.Deref.html | ||
[deref-mut-doc]: https://doc.rust-lang.org/core/ops/trait.DerefMut.html |
Uh oh!
There was an error while loading. Please reload this page.