-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
a29fff0
Add example of analyzing soundness of Send/Sync
dzfranklin 8739b01
Don't execute example code
dzfranklin c3ba178
Fix soundness issues in new
dzfranklin 05a2df5
Fix missing trait bounds
dzfranklin 29f7139
Reduce untested code
dzfranklin 2dd3566
Use american spelling
dzfranklin 357eab4
Fix formatting
dzfranklin e542c32
Add heading
dzfranklin 75253ec
Make out param stack local, fix for zero sized types
dzfranklin 53661a5
Description fixes by danielhenrymantilla
dzfranklin 9f9f909
Qualify with ptr::
dzfranklin a5da003
Fix memory leak and discuss implications
dzfranklin d060a51
Reword
dzfranklin 09d4be2
Update src/send-and-sync.md
dzfranklin 484bbeb
Update src/send-and-sync.md
dzfranklin d1c89a8
Update src/send-and-sync.md
dzfranklin 7a1be9d
Fix failing tests
dzfranklin 33fa3de
Update src/send-and-sync.md
dzfranklin 4a910d8
Update src/send-and-sync.md
dzfranklin 047dc6b
Update src/send-and-sync.md
dzfranklin 6495701
Update src/send-and-sync.md
dzfranklin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,7 +74,184 @@ 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 | ||
# pub mod libc { | ||
# pub use ::std::os::raw::{c_int, c_void}; | ||
# #[allow(non_camel_case_types)] | ||
# pub type size_t = usize; | ||
# extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; } | ||
# } | ||
use std::{ | ||
mem::{align_of, size_of}, | ||
ptr, | ||
}; | ||
|
||
struct Carton<T>(ptr::NonNull<T>); | ||
|
||
impl<T> Carton<T> { | ||
pub fn new(value: T) -> Self { | ||
// Allocate enough memory on the heap to store one T. | ||
assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example"); | ||
let mut memptr = ptr::null_mut() as *mut T; | ||
unsafe { | ||
let ret = libc::posix_memalign( | ||
(&mut memptr).cast(), | ||
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. | ||
ptr::NonNull::new(memptr.cast::<T>()) | ||
.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>); | ||
# | ||
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 | ||
// 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, let's think about whether our `Carton` is Send and Sync. Something can | ||
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 {} | ||
``` | ||
|
||
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: Since there exists a public way to go from a `&Carton<T>` to a `&T` | ||
// in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be | ||
// `Sync` if `T` isn't. | ||
// Conversely, `Carton` itself does not use any interior mutability whatsoever: | ||
// all the mutations are performed through an exclusive reference (`&mut`). This | ||
// means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`: | ||
unsafe impl<T> Sync for Carton<T> where T: Sync {} | ||
``` | ||
|
||
When we assert our type is Send and Sync we usually 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 {} | ||
``` | ||
|
||
Right now `Carton<T>` has a memory leak, as it never frees the memory it allocates. | ||
Once we fix that we have a new requirement we have to ensure we meet to be Send: | ||
we need to know `free` can be called on a pointer that was yielded by an | ||
allocation done on another thread. We can check this is true in the docs for | ||
[`libc::free`][libc-free-docs]. | ||
|
||
```rust | ||
# struct Carton<T>(std::ptr::NonNull<T>); | ||
# mod libc { | ||
# pub use ::std::os::raw::c_void; | ||
# extern "C" { pub fn free(p: *mut c_void); } | ||
# } | ||
impl<T> Drop for Carton<T> { | ||
fn drop(&mut self) { | ||
unsafe { | ||
libc::free(self.0.as_ptr().cast()); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
A nice example where this does not happen is with a MutexGuard: notice how | ||
[it is not Send][mutex-guard-not-send-docs-rs]. The implementation of MutexGuard | ||
[uses libraries][mutex-guard-not-send-comment] that require you to ensure you | ||
don't try to free a lock that you acquired in a different thread. If you were | ||
able to Send a MutexGuard to another thread the destructor would run in the | ||
thread you sent it to, violating the requirement. MutexGuard can still be Sync | ||
because all you can send to another thread is an `&MutexGuard` and dropping a | ||
reference does nothing. | ||
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. 👌 |
||
|
||
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 | ||
[mutex-guard-not-send-docs-rs]: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html#impl-Send | ||
[mutex-guard-not-send-comment]: https://github.com/rust-lang/rust/issues/23465#issuecomment-82730326 | ||
[libc-free-docs]: https://linux.die.net/man/3/free |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.