Skip to content

Commit

Permalink
Implement stack tokens to avoid unsound behavior (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Oct 18, 2022
1 parent 0a99af0 commit 1b1304d
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 86 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,9 +2,11 @@

All notable changes to similar are documented here.

## 1.3.0
## 2.0.0

* `Fragile` no longer boxes internally.
* `Sticky` and `SemiSticky` now require the use of stack tokens.
For more information see [#26](https://github.com/mitsuhiko/fragile/issues/26)

## 1.2.1

Expand Down
17 changes: 9 additions & 8 deletions src/fragile.rs
Expand Up @@ -7,14 +7,15 @@ use crate::errors::InvalidThreadAccess;
use crate::thread_id;
use std::mem::ManuallyDrop;

/// A `Fragile<T>` wraps a non sendable `T` to be safely send to other threads.
/// A [`Fragile<T>`] wraps a non sendable `T` to be safely send to other threads.
///
/// Once the value has been wrapped it can be sent to other threads but access
/// to the value on those threads will fail.
///
/// If the value needs destruction and the fragile wrapper is on another thread
/// the destructor will panic. Alternatively you can use `Sticky<T>` which is
/// not going to panic but might temporarily leak the value.
/// the destructor will panic. Alternatively you can use
/// [`Sticky`](crate::Sticky) which is not going to panic but might temporarily
/// leak the value.
pub struct Fragile<T> {
// ManuallyDrop is necessary because we need to move out of here without running the
// Drop code in functions like `into_inner`.
Expand All @@ -23,9 +24,9 @@ pub struct Fragile<T> {
}

impl<T> Fragile<T> {
/// Creates a new `Fragile` wrapping a `value`.
/// Creates a new [`Fragile`] wrapping a `value`.
///
/// The value that is moved into the `Fragile` can be non `Send` and
/// The value that is moved into the [`Fragile`] can be non `Send` and
/// will be anchored to the thread that created the object. If the
/// fragile wrapper type ends up being send from thread to thread
/// only the original thread can interact with the value.
Expand Down Expand Up @@ -70,7 +71,7 @@ impl<T> Fragile<T> {
///
/// The wrapped value is returned if this is called from the same thread
/// as the one where the original value was created, otherwise the
/// `Fragile` is returned as `Err(self)`.
/// [`Fragile`] is returned as `Err(self)`.
pub fn try_into_inner(self) -> Result<T, Self> {
if thread_id::get() == self.thread_id {
Ok(self.into_inner())
Expand All @@ -84,7 +85,7 @@ impl<T> Fragile<T> {
/// # Panics
///
/// Panics if the calling thread is not the one that wrapped the value.
/// For a non-panicking variant, use [`try_get`](#method.try_get`).
/// For a non-panicking variant, use [`try_get`](Self::try_get).
pub fn get(&self) -> &T {
self.assert_thread();
&*self.value
Expand All @@ -95,7 +96,7 @@ impl<T> Fragile<T> {
/// # Panics
///
/// Panics if the calling thread is not the one that wrapped the value.
/// For a non-panicking variant, use [`try_get_mut`](#method.try_get_mut`).
/// For a non-panicking variant, use [`try_get_mut`](Self::try_get_mut).
pub fn get_mut(&mut self) -> &mut T {
self.assert_thread();
&mut *self.value
Expand Down
92 changes: 85 additions & 7 deletions src/lib.rs
@@ -1,26 +1,32 @@
//! This library provides wrapper types that permit sending non `Send` types to
//! other threads and use runtime checks to ensure safety.
//!
//! It provides three types: `Fragile<T>` and `Sticky<T>` which are similar in nature
//! It provides three types: [`Fragile`] and [`Sticky`] which are similar in nature
//! but have different behaviors with regards to how destructors are executed and
//! the extra `SemiSticky<T>` type which uses `Sticky<T>` if the value has a
//! destructor and `Fragile<T>` if it does not.
//! the extra [`SemiSticky`] type which uses [`Sticky`] if the value has a
//! destructor and [`Fragile`] if it does not.
//!
//! Both types wrap a value and provide a `Send` bound. Neither of the types permit
//! access to the enclosed value unless the thread that wrapped the value is attempting
//! to access it. The difference between the two types starts playing a role once
//! destructors are involved.
//!
//! A `Fragile<T>` will actually send the `T` from thread to thread but will only
//! A [`Fragile`] will actually send the `T` from thread to thread but will only
//! permit the original thread to invoke the destructor. If the value gets dropped
//! in a different thread, the destructor will panic.
//!
//! A `Sticky<T>` on the other hand does not actually send the `T` around but keeps
//! A [`Sticky`] on the other hand does not actually send the `T` around but keeps
//! it stored in the original thread's thread local storage. If it gets dropped
//! in the originating thread it gets cleaned up immediately, otherwise it leaks
//! until the thread shuts down naturally.
//! until the thread shuts down naturally. [`Sticky`] (and by extension
//! [`SemiSticky`]) because it borrows into the TLS also requires you to
//! "prove" that you are not doing any funny business with the borrowed value
//! that lives for longer than the current stack frame which results in a slightly
//! more complex API.
//!
//! # Example usage
//! # Fragile Usage
//!
//! [`Fragile`] is the easiest type to use. It works almost like a cell.
//!
//! ```
//! use std::thread;
Expand All @@ -38,6 +44,32 @@
//! .unwrap();
//! ```
//!
//! # Sticky Usage
//!
//! [`Sticky`] is similar to [`Fragile`] but because it places the value in the
//! thread local storage it comes with some extra restrictions to make it sound.
//! The advantage is it can be dropped from any thread but it comes with extra
//! restrictions. In particular it requires that values placed in it are `'static`
//! and that [`StackToken`]s are used to restrict lifetimes.
//!
//! ```
//! use std::thread;
//! use fragile::Sticky;
//!
//! // creating and using a fragile object in the same thread works
//! fragile::stack_token!(tok);
//! let val = Sticky::new(true);
//! assert_eq!(*val.get(tok), true);
//! assert!(val.try_get(tok).is_ok());
//!
//! // once send to another thread it stops working
//! thread::spawn(move || {
//! fragile::stack_token!(tok);
//! assert!(val.try_get(tok).is_err());
//! }).join()
//! .unwrap();
//! ```
//!
//! # Why?
//!
//! Most of the time trying to use this crate is going to indicate some code smell. But
Expand All @@ -62,3 +94,49 @@ pub use crate::errors::InvalidThreadAccess;
pub use crate::fragile::Fragile;
pub use crate::semisticky::SemiSticky;
pub use crate::sticky::Sticky;

/// A token that is placed to the stack to constrain lifetimes.
///
/// For more information about how these work see the documentation of
/// [`stack_token!`] which is the only way to create this token.
pub struct StackToken(*const ());

impl StackToken {
/// Stack tokens must only be created on the stack.
#[doc(hidden)]
pub unsafe fn __private_new() -> StackToken {
// we place a const pointer in there to get a type
// that is neither Send nor Sync.
StackToken(std::ptr::null())
}
}

/// Crates a token on the stack with a certain name for semi-sticky.
///
/// The argument to the macro is the target name of a local variable
/// which holds a reference to a stack token. Because this is the
/// only way to create such a token, it acts as a proof to [`Sticky`]
/// or [`SemiSticky`] that can be used to constrain the lifetime of the
/// return values to the stack frame.
///
/// This is necessary as otherwise a [`Sticky`] placed in a [`Box`] and
/// leaked with [`Box::leak`] (which creates a static lifetime) would
/// otherwise create a reference with `'static` lifetime. This is incorrect
/// as the actual lifetime is constrained to the lifetime of the thread.
/// For more information see [`issue 26`](https://github.com/mitsuhiko/fragile/issues/26).
///
/// ```rust
/// let sticky = fragile::Sticky::new(true);
///
/// // this places a token on the stack.
/// fragile::stack_token!(my_token);
///
/// // the token needs to be passed to `get` and others.
/// let _ = sticky.get(my_token);
/// ```
#[macro_export]
macro_rules! stack_token {
($name:ident) => {
let $name = &unsafe { $crate::StackToken::__private_new() };
};
}

0 comments on commit 1b1304d

Please sign in to comment.