diff --git a/src/send-and-sync.md b/src/send-and-sync.md index 87d0d300..97423282 100644 --- a/src/send-and-sync.md +++ b/src/send-and-sync.md @@ -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(ptr::NonNull); + +impl Carton { + pub fn new(value: T) -> Self { + // Allocate enough memory on the heap to store one T. + assert_ne!(size_of::(), 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::(), + size_of::() + ); + 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::()) + .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(std::ptr::NonNull); +# +impl Deref for Carton { + 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 DerefMut for Carton { + 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(std::ptr::NonNull); +// 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 Send for Carton 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(std::ptr::NonNull); +// Safety: Since there exists a public way to go from a `&Carton` to a `&T` +// in an unsynchronized fashion (such as `Deref`), then `Carton` 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` to be `Sync`: +unsafe impl Sync for Carton 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(std::ptr::NonNull); +unsafe impl Send for Carton where Box: Send {} +``` + +Right now `Carton` 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(std::ptr::NonNull); +# mod libc { +# pub use ::std::os::raw::c_void; +# extern "C" { pub fn free(p: *mut c_void); } +# } +impl Drop for Carton { + 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. + 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