Skip to content

Commit 8551afb

Browse files
dzfranklindanielhenrymantillaJohnTitor
authored
Add example of thinking about Send/Sync's soundness (#259)
Co-authored-by: Daniel Henry-Mantilla <[email protected]> Co-authored-by: Yuki Okushi <[email protected]>
1 parent 6fe4769 commit 8551afb

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

src/send-and-sync.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,184 @@ of their pervasive use of raw pointers to manage allocations and complex ownersh
7474
Similarly, most iterators into these collections are Send and Sync because they
7575
largely behave like an `&` or `&mut` into the collection.
7676

77+
## Example
78+
79+
[`Box`][box-doc] is implemented as it's own special intrinsic type by the
80+
compiler for [various reasons][box-is-special], but we can implement something
81+
with similar-ish behavior ourselves to see an example of when it is sound to
82+
implement Send and Sync. Let's call it a `Carton`.
83+
84+
We start by writing code to take a value allocated on the stack and transfer it
85+
to the heap.
86+
87+
```rust
88+
# pub mod libc {
89+
# pub use ::std::os::raw::{c_int, c_void};
90+
# #[allow(non_camel_case_types)]
91+
# pub type size_t = usize;
92+
# extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; }
93+
# }
94+
use std::{
95+
mem::{align_of, size_of},
96+
ptr,
97+
};
98+
99+
struct Carton<T>(ptr::NonNull<T>);
100+
101+
impl<T> Carton<T> {
102+
pub fn new(value: T) -> Self {
103+
// Allocate enough memory on the heap to store one T.
104+
assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example");
105+
let mut memptr = ptr::null_mut() as *mut T;
106+
unsafe {
107+
let ret = libc::posix_memalign(
108+
(&mut memptr).cast(),
109+
align_of::<T>(),
110+
size_of::<T>()
111+
);
112+
assert_eq!(ret, 0, "Failed to allocate or invalid alignment");
113+
};
114+
115+
// NonNull is just a wrapper that enforces that the pointer isn't null.
116+
let mut ptr = unsafe {
117+
// Safety: memptr is dereferenceable because we created it from a
118+
// reference and have exclusive access.
119+
ptr::NonNull::new(memptr.cast::<T>())
120+
.expect("Guaranteed non-null if posix_memalign returns 0")
121+
};
122+
123+
// Move value from the stack to the location we allocated on the heap.
124+
unsafe {
125+
// Safety: If non-null, posix_memalign gives us a ptr that is valid
126+
// for writes and properly aligned.
127+
ptr.as_ptr().write(value);
128+
}
129+
130+
Self(ptr)
131+
}
132+
}
133+
```
134+
135+
This isn't very useful, because once our users give us a value they have no way
136+
to access it. [`Box`][box-doc] implements [`Deref`][deref-doc] and
137+
[`DerefMut`][deref-mut-doc] so that you can access the inner value. Let's do
138+
that.
139+
140+
```rust
141+
use std::ops::{Deref, DerefMut};
142+
143+
# struct Carton<T>(std::ptr::NonNull<T>);
144+
#
145+
impl<T> Deref for Carton<T> {
146+
type Target = T;
147+
148+
fn deref(&self) -> &Self::Target {
149+
unsafe {
150+
// Safety: The pointer is aligned, initialized, and dereferenceable
151+
// by the logic in [`Self::new`]. We require writers to borrow the
152+
// Carton, and the lifetime of the return value is elided to the
153+
// lifetime of the input. This means the borrow checker will
154+
// enforce that no one can mutate the contents of the Carton until
155+
// the reference returned is dropped.
156+
self.0.as_ref()
157+
}
158+
}
159+
}
160+
161+
impl<T> DerefMut for Carton<T> {
162+
fn deref_mut(&mut self) -> &mut Self::Target {
163+
unsafe {
164+
// Safety: The pointer is aligned, initialized, and dereferenceable
165+
// by the logic in [`Self::new`]. We require writers to mutably
166+
// borrow the Carton, and the lifetime of the return value is
167+
// elided to the lifetime of the input. This means the borrow
168+
// checker will enforce that no one else can access the contents
169+
// of the Carton until the mutable reference returned is dropped.
170+
self.0.as_mut()
171+
}
172+
}
173+
}
174+
```
175+
176+
Finally, let's think about whether our `Carton` is Send and Sync. Something can
177+
safely be Send unless it shares mutable state with something else without
178+
enforcing exclusive access to it. Each `Carton` has a unique pointer, so
179+
we're good.
180+
181+
```rust
182+
# struct Carton<T>(std::ptr::NonNull<T>);
183+
// Safety: No one besides us has the raw pointer, so we can safely transfer the
184+
// Carton to another thread if T can be safely transferred.
185+
unsafe impl<T> Send for Carton<T> where T: Send {}
186+
```
187+
188+
What about Sync? For `Carton` to be Sync we have to enforce that you can't
189+
write to something stored in a `&Carton` while that same something could be read
190+
or written to from another `&Carton`. Since you need an `&mut Carton` to
191+
write to the pointer, and the borrow checker enforces that mutable
192+
references must be exclusive, there are no soundness issues making `Carton`
193+
sync either.
194+
195+
```rust
196+
# struct Carton<T>(std::ptr::NonNull<T>);
197+
// Safety: Since there exists a public way to go from a `&Carton<T>` to a `&T`
198+
// in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be
199+
// `Sync` if `T` isn't.
200+
// Conversely, `Carton` itself does not use any interior mutability whatsoever:
201+
// all the mutations are performed through an exclusive reference (`&mut`). This
202+
// means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`:
203+
unsafe impl<T> Sync for Carton<T> where T: Sync {}
204+
```
205+
206+
When we assert our type is Send and Sync we usually need to enforce that every
207+
contained type is Send and Sync. When writing custom types that behave like
208+
standard library types we can assert that we have the same requirements.
209+
For example, the following code asserts that a Carton is Send if the same
210+
sort of Box would be Send, which in this case is the same as saying T is Send.
211+
212+
```rust
213+
# struct Carton<T>(std::ptr::NonNull<T>);
214+
unsafe impl<T> Send for Carton<T> where Box<T>: Send {}
215+
```
216+
217+
Right now `Carton<T>` has a memory leak, as it never frees the memory it allocates.
218+
Once we fix that we have a new requirement we have to ensure we meet to be Send:
219+
we need to know `free` can be called on a pointer that was yielded by an
220+
allocation done on another thread. We can check this is true in the docs for
221+
[`libc::free`][libc-free-docs].
222+
223+
```rust
224+
# struct Carton<T>(std::ptr::NonNull<T>);
225+
# mod libc {
226+
# pub use ::std::os::raw::c_void;
227+
# extern "C" { pub fn free(p: *mut c_void); }
228+
# }
229+
impl<T> Drop for Carton<T> {
230+
fn drop(&mut self) {
231+
unsafe {
232+
libc::free(self.0.as_ptr().cast());
233+
}
234+
}
235+
}
236+
```
237+
238+
A nice example where this does not happen is with a MutexGuard: notice how
239+
[it is not Send][mutex-guard-not-send-docs-rs]. The implementation of MutexGuard
240+
[uses libraries][mutex-guard-not-send-comment] that require you to ensure you
241+
don't try to free a lock that you acquired in a different thread. If you were
242+
able to Send a MutexGuard to another thread the destructor would run in the
243+
thread you sent it to, violating the requirement. MutexGuard can still be Sync
244+
because all you can send to another thread is an `&MutexGuard` and dropping a
245+
reference does nothing.
246+
77247
TODO: better explain what can or can't be Send or Sync. Sufficient to appeal
78248
only to data races?
79249

80250
[unsafe traits]: safe-unsafe-meaning.html
251+
[box-doc]: https://doc.rust-lang.org/std/boxed/struct.Box.html
252+
[box-is-special]: https://manishearth.github.io/blog/2017/01/10/rust-tidbits-box-is-special/
253+
[deref-doc]: https://doc.rust-lang.org/core/ops/trait.Deref.html
254+
[deref-mut-doc]: https://doc.rust-lang.org/core/ops/trait.DerefMut.html
255+
[mutex-guard-not-send-docs-rs]: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html#impl-Send
256+
[mutex-guard-not-send-comment]: https://github.com/rust-lang/rust/issues/23465#issuecomment-82730326
257+
[libc-free-docs]: https://linux.die.net/man/3/free

0 commit comments

Comments
 (0)