use std::cell::UnsafeCell;
use std::panic::{self, AssertUnwindSafe};
use std::{fmt, ptr, thread};
use crate::primitives::{AtomicUsize, Ordering};
const PENDING: usize = 0;
const SLEEPY: usize = 1;
const SET: usize = 2;
pub const NO_OWNER: usize = usize::MAX;
struct Frame<F, R> {
latch: AtomicUsize,
owner: usize,
f: UnsafeCell<Option<F>>,
result: UnsafeCell<Option<thread::Result<R>>>,
}
#[derive(Clone, Copy)]
enum Mode {
Owner,
Thief,
Cancel,
}
pub struct StackJob {
frame: *const (),
call: unsafe fn(*const (), Mode) -> Option<usize>,
pending: bool,
}
unsafe impl Send for StackJob {}
impl fmt::Debug for StackJob {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StackJob")
.field("id", &self.id())
.field("pending", &self.pending)
.finish()
}
}
impl StackJob {
#[must_use]
pub fn id(&self) -> usize {
self.frame.addr()
}
#[must_use = "an owner that is about to sleep must be woken"]
pub fn run(mut self) -> Option<usize> {
self.pending = false;
unsafe { (self.call)(self.frame, Mode::Thief) }
}
pub fn run_inline(mut self) {
self.pending = false;
let _ = unsafe { (self.call)(self.frame, Mode::Owner) };
}
}
impl Drop for StackJob {
fn drop(&mut self) {
if self.pending {
self.pending = false;
let _ = unsafe { (self.call)(self.frame, Mode::Cancel) };
}
}
}
unsafe fn call<F: FnOnce() -> R, R>(ptr: *const (), mode: Mode) -> Option<usize> {
let frame = unsafe { &*ptr.cast::<Frame<F, R>>() };
let owner = frame.owner;
let f = unsafe { (*frame.f.get()).take() };
match (mode, f) {
(Mode::Cancel, f) => {
let _ = panic::catch_unwind(AssertUnwindSafe(|| drop(f)));
}
(_, Some(f)) => {
let result = panic::catch_unwind(AssertUnwindSafe(f));
unsafe { *frame.result.get() = Some(result) };
}
(_, None) => {}
}
match mode {
Mode::Owner => {
frame.latch.store(SET, Ordering::Release);
None
}
Mode::Thief | Mode::Cancel => {
let previous = frame.latch.swap(SET, Ordering::AcqRel);
(previous == SLEEPY && owner != NO_OWNER).then_some(owner)
}
}
}
#[derive(Debug)]
pub struct JoinLatch<'a> {
state: &'a AtomicUsize,
}
impl JoinLatch<'_> {
#[must_use]
pub fn is_set(&self) -> bool {
self.state.load(Ordering::Acquire) == SET
}
#[must_use]
pub fn prepare_park(&self) -> bool {
match self
.state
.compare_exchange(PENDING, SLEEPY, Ordering::AcqRel, Ordering::Acquire)
{
Ok(_) => true,
Err(state) => state != SET,
}
}
}
pub fn join_on_stack<B, RB, T>(
owner: usize,
b: B,
body: impl FnOnce(StackJob) -> T,
mut wait: impl FnMut(&JoinLatch<'_>),
) -> (T, Option<thread::Result<RB>>)
where
B: FnOnce() -> RB + Send,
RB: Send,
{
let frame = Frame {
latch: AtomicUsize::new(PENDING),
owner,
f: UnsafeCell::new(Some(b)),
result: UnsafeCell::new(None),
};
let job = StackJob {
frame: ptr::from_ref(&frame).cast::<()>(),
call: call::<B, RB>,
pending: true,
};
let out = panic::catch_unwind(AssertUnwindSafe(|| body(job)));
let latch = JoinLatch {
state: &frame.latch,
};
let waited = panic::catch_unwind(AssertUnwindSafe(|| {
while !latch.is_set() {
wait(&latch);
}
}));
if waited.is_err() {
std::process::abort();
}
let result = frame.result.into_inner();
match out {
Ok(value) => (value, result),
Err(payload) => {
drop(result);
panic::resume_unwind(payload)
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc;
#[test]
fn inline_run_needs_no_wait() {
let mut data = [0_u32; 4];
let (left, right) = data.split_at_mut(2);
let (out, b) = join_on_stack(
NO_OWNER,
|| right.fill(2),
|job| {
left.fill(1);
job.run_inline();
7
},
|_| panic!("latch is set by run_inline"),
);
assert_eq!(out, 7);
assert!(matches!(b, Some(Ok(()))));
assert_eq!(data, [1, 1, 2, 2]);
}
#[test]
fn thief_runs_on_other_thread_and_borrows_stack() {
let value = String::from("borrowed");
let (tx, rx) = mpsc::channel::<StackJob>();
let thief = std::thread::spawn(move || {
let job = rx.recv().unwrap();
job.run()
});
let ((), b) = join_on_stack(
3,
|| value.len(),
|job| tx.send(job).unwrap(),
|_| std::thread::yield_now(),
);
assert_eq!(b.and_then(Result::ok), Some(8));
assert_eq!(thief.join().unwrap(), None);
}
#[test]
fn sleepy_owner_is_reported_for_wakeup() {
let (tx, rx) = mpsc::channel::<StackJob>();
let (woken_tx, woken_rx) = mpsc::channel();
let parked = &AtomicBool::new(false);
std::thread::scope(|s| {
s.spawn(move || {
let job = rx.recv().unwrap();
while !parked.load(std::sync::atomic::Ordering::SeqCst) {
std::thread::yield_now();
}
woken_tx.send(job.run()).unwrap();
});
let ((), b) = join_on_stack(
5,
|| 1,
|job| tx.send(job).unwrap(),
|latch| {
if latch.prepare_park() {
parked.store(true, std::sync::atomic::Ordering::SeqCst);
}
std::thread::yield_now();
},
);
assert!(matches!(b, Some(Ok(1))));
});
assert_eq!(woken_rx.recv().unwrap(), Some(5));
}
#[test]
fn dropped_job_is_cancelled() {
let ((), b) = join_on_stack(NO_OWNER, || 1, drop, |_| {});
assert!(b.is_none());
}
#[test]
fn panic_in_b_is_returned() {
let ((), b) = join_on_stack(
NO_OWNER,
|| -> u32 { panic!("b failed") },
StackJob::run_inline,
|_| {},
);
assert!(matches!(b, Some(Err(_))));
}
#[test]
fn panic_in_body_waits_for_b_then_propagates() {
let ran = AtomicBool::new(false);
let (tx, rx) = mpsc::channel::<StackJob>();
let result = panic::catch_unwind(AssertUnwindSafe(|| {
join_on_stack(
NO_OWNER,
|| ran.store(true, std::sync::atomic::Ordering::SeqCst),
|job| {
tx.send(job).unwrap();
panic!("a failed");
},
|_| {
if let Ok(job) = rx.try_recv() {
let _ = job.run();
}
},
)
}));
assert!(result.is_err());
assert!(ran.load(std::sync::atomic::Ordering::SeqCst));
}
}