Skip to content
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

Draft: Add Send and Sync #19

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ impl<'a> nolife::Family<'a> for MyParsedDataFamily {
// 2. Define a function that setups the data and its borrowed representation:
fn my_scope(
data_source: Vec<u8>, // 👈 all parameters that allow to build a `MyData`
) -> impl nolife::TopScope<Family = MyParsedDataFamily> // 👈 use the helper type we declared
) -> impl nolife::TopScope<Family = MyParsedDataFamily, Future = impl Send + Sync> // 👈 use the helper type we declared
{
nolife::scope!({
let mut data = MyData(data_source);
Expand Down
57 changes: 56 additions & 1 deletion src/box_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{raw_scope::RawScope, Family, Never, TopScope};
/// This kind of scopes uses a dynamic allocation.
/// In exchange, it is fully `'static` and can be moved after creation.
#[repr(transparent)]
pub struct BoxScope<T, F: ?Sized = dyn Future<Output = Never> + 'static>(
pub struct BoxScope<T, F: ?Sized = dyn Future<Output = Never> + Send + 'static>(
core::ptr::NonNull<RawScope<T, F>>,
)
where
Expand Down Expand Up @@ -50,10 +50,41 @@ where
///
/// If the `Future` generic type can be inferred, it can be more efficient to use [`BoxScope::new`].
///
/// This function requires that the passed `Future` is [`Send`] and [`Sync`]. If it is not the case,
/// use [`BoxScope::new_local_dyn`].
///
/// # Panics
///
/// - If `scope` panics.
pub fn new_dyn<S: TopScope<Family = T>>(scope: S) -> Self
where
S::Future: Send + 'static,
{
let this = mem::ManuallyDrop::new(BoxScope::new(scope));
Self(this.0)
}
}

impl<T> BoxScope<T, dyn Future<Output = Never>>
where
T: for<'a> Family<'a>,
{
/// Ties the passed scope to the heap.
///
/// This function erased the `Future` generic type of the [`TopScope`], at the cost
/// of using a dynamic function call to poll the future.
///
/// If the `Future` generic type can be inferred, it can be more efficient to use [`BoxScope::new`].
///
/// Further, this function erased the thread-safety-ness of the underlying future type.
/// Unless your `Future` is not [`Send`] or not [`Sync`], prefer [`BoxScope::new_dyn`].
///
/// The `BoxScope` resulting from calling this function will also not be [`Send`] or [`Sync`].
///
/// # Panics
///
/// - If `scope` panics.
pub fn new_local_dyn<S: TopScope<Family = T>>(scope: S) -> Self
where
S::Future: 'static,
{
Expand Down Expand Up @@ -135,3 +166,27 @@ where
unsafe { RawScope::enter(self.0, f) }
}
}

// SAFETY:
//
// - No operation can be performed on a `&BoxScope`, so it is trivially `Sync`
// - Operations that require a `&BoxScope` may require that the Family or Future be Sync as well.
unsafe impl<T, F> Sync for BoxScope<T, F>
where
T: for<'a> Family<'a>,
F: Future<Output = Never> + ?Sized,
{
}

// SAFETY:
//
// - `BoxScope` has owning semantic on its inner `RawScope`, so `BoxScope` is `Send`
// if and only if its inner `RawScope` is safe.
//
// Meanwhile `RawScope` is `Send` if its `F` is `Send`.
unsafe impl<T, F> Send for BoxScope<T, F>
where
T: for<'a> Family<'a>,
F: Future<Output = Never> + ?Sized + Send,
{
}
68 changes: 65 additions & 3 deletions src/counterexamples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
//!
//! # Dropping a borrowed input to a scope.
//!
//! ```compile_fail,E505
//! ```compile_fail,E0505
//! use nolife::{scope, BoxScope, SingleFamily, TopScope};
//!
//! fn ref_scope() {
Expand All @@ -369,13 +369,13 @@
//!
//! # Dropping a borrowed input to a scope, erased version
//!
//! ```compile_fail,E597,E505
//! ```compile_fail,E0597,E0505
//! use nolife::{scope, BoxScope, SingleFamily, TopScope};
//!
//! fn ref_scope() {
//! fn scope_with_ref<'scope, 'a: 'scope>(
//! s: &'a str,
//! ) -> impl TopScope<Family = SingleFamily<usize>> + 'scope {
//! ) -> impl TopScope<Family = SingleFamily<usize>, Future = impl Send + Sync + 'a> + 'scope {
//! scope!({ freeze_forever!(&mut s.len()) })
//! }
//! let x = "Intel the Beagle".to_string();
Expand All @@ -386,3 +386,65 @@
//! scope.enter(|x| assert_eq!(*x, 16));
//! }
//! ```
//!
//! # Trying to Send with a non-Send Future
//!
//! ```compile_fail
//! let mut scope = nolife::BoxScope::<nolife::SingleFamily<u32>, _>::new(nolife::scope!({
//! let rc = std::rc::Rc::new(42);
//! let mut x = 0u32;
//! loop {
//! freeze!(&mut x);
//!
//! x += 1;
//! }
//! }));
//!
//! std::thread::scope(|t_scope| {
//! t_scope.spawn(|| {
//! assert_eq!(scope.enter(|x| *x + 42), 42);
//! assert_eq!(scope.enter(|x| *x + 42), 43);
//! scope.enter(|x| *x += 100);
//! assert_eq!(scope.enter(|x| *x + 42), 145);
//! });
//! })
//! ```
//!
//! # Trying to Send with a non-send Family
//!
//! ```compile_fail
//! let rc = std::rc::Rc::new(42);
//! let rc_clone = rc.clone();
//! let mut scope = nolife::BoxScope::<nolife::SingleFamily<std::rc::Rc<u32>>, _>::new(nolife::scope!({
//! freeze_forever!(&mut rc_clone)
//! }));
//!
//! std::thread::scope(|t_scope| {
//! t_scope.spawn(|| {
//! scope.enter(|_| {});
//! });
//! })
//! ```
//!
//! # Trying to send the time capsule or frozenfuture
//!
//! ```compile_fail,E0728
//! let mut scope = nolife::BoxScope::<nolife::SingleFamily<u32>, _>::new(nolife::scope!({
//! let rc = std::rc::Rc::new(42);
//! let mut x = 0u32;
//! loop {
//! std::thread::scope(|t_scope| {
//! t_scope.spawn(|| {
//! freeze!(&mut x);
//! });
//! });
//! x += 1;
//! }
//! }));
//!
//! assert_eq!(scope.enter(|x| *x + 42), 42);
//! assert_eq!(scope.enter(|x| *x + 42), 43);
//! scope.enter(|x| *x += 100);
//! assert_eq!(scope.enter(|x| *x + 42), 145);
//! ```
//!
76 changes: 76 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,80 @@ mod test {

must_panic(|| scope.enter(|x| assert_eq!(*x, 42)));
}

#[test]
#[cfg(feature = "std")]
fn send_in_thread() {
let mut scope = BoxScope::<SingleFamily<u32>, _>::new(scope!({
let mut x = 0u32;
loop {
freeze!(&mut x);

x += 1;
}
}));

std::thread::scope(|t_scope| {
t_scope.spawn(|| {
assert_eq!(scope.enter(|x| *x + 42), 42);
assert_eq!(scope.enter(|x| *x + 42), 43);
scope.enter(|x| *x += 100);
assert_eq!(scope.enter(|x| *x + 42), 145);
});
})
}

#[test]
#[cfg(feature = "std")]
fn sync_in_thread() {
let scope = BoxScope::<SingleFamily<u32>, _>::new(scope!({
let mut x = 0u32;
loop {
freeze!(&mut x);
x += 1;
}
}));

let scope_ref = &scope;

std::thread::scope(|t_scope| {
t_scope.spawn(|| scope_ref);
})
}

#[test]
#[cfg(feature = "std")]
fn non_sync_family_in_thread() {
let rc = std::rc::Rc::new(42);
let mut rc_clone = rc.clone();
let scope: BoxScope<_, _> = BoxScope::<SingleFamily<std::rc::Rc<u32>>, _>::new(scope!({
freeze_forever!(&mut rc_clone)
}));

let scope_ref = &scope;

std::thread::scope(|t_scope| {
t_scope.spawn(|| scope_ref);
})
}

#[test]
#[cfg(feature = "std")]
fn non_sync_fut_in_thread() {
let rc = std::rc::Rc::new(42);
let rc_cloned = rc.clone();
let scope = BoxScope::<SingleFamily<u32>, _>::new(scope!({
loop {
let rc = rc_cloned.clone();
let mut rc_ref = *rc;

freeze!(&mut rc_ref);
}
}));

let scope_ref = &scope;
std::thread::scope(|t_scope| {
t_scope.spawn(|| scope_ref);
})
}
}
42 changes: 42 additions & 0 deletions src/raw_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,48 @@ where
// `<T as Family<'static>>::Family>` has T invariant already anyway.
pub(crate) type State<T> = Option<NonNull<<T as Family<'static>>::Family>>;

// SAFETY:
//
// 1. `T::Family` can be sent between threads
// 2. As `TimeCapsule` is `Copy`, its `T::Family` *may* actually be accessed unsynchronized
// between threads. That it is not the case is a soundness precondition, verified if using the
// `scope!` macro.
//
// Note: required for `F` to be `Send`
unsafe impl<T> Send for TimeCapsule<T>
where
T: for<'a> Family<'a>,
for<'a> <T as Family<'a>>::Family: Send,
{
}

// SAFETY:
//
// - Trivial as no operation can be performed on a `&TimeCapsule`
// Note: required for `F` to be `Sync`
unsafe impl<T> Sync for TimeCapsule<T> where T: for<'a> Family<'a> {}

// SAFETY:
//
// 1. `T::Family` can be sent between threads
// 2. `FrozenFuture` does provide unsynchronized write access to a shared resource, so a
// soundness precondition is that it cannot actually be accessed unsynchronized from multiple threads.
// Using the `scope!` macro, it is always verified.
//
// Note: required for `F` to be `Send`
unsafe impl<'a, 'b, T> Send for FrozenFuture<'a, 'b, T>
where
T: for<'c> Family<'c>,
for<'c> <T as Family<'c>>::Family: Send,
{
}

// SAFETY:
//
// - Trivial as no operation can be performed on a `&FrozenFuture`
// Note: required for `F` to be `Sync`
unsafe impl<'a, 'b, T> Sync for FrozenFuture<'a, 'b, T> where T: for<'c> Family<'c> {}

/// Underlying representation of a scope.
// SAFETY: repr C to ensure conversion between RawScope<T, MaybeUninit<F>> and RawScope<T, F>
// does not rely on unstable memory layout.
Expand Down
Loading