use core::ffi::c_int;
use core::sync::atomic::{AtomicU32, Ordering};
const FUTEX_BLOCKED: u32 = 0;
const FUTEX_RUNNING: u32 = 1;
const FUTEX_WAIT_PRIVATE: c_int = libc::FUTEX_WAIT | libc::FUTEX_PRIVATE_FLAG;
const FUTEX_WAKE_PRIVATE: c_int = libc::FUTEX_WAKE | libc::FUTEX_PRIVATE_FLAG;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WaitOutcome {
Posted,
TimedOut,
}
pub(crate) struct Handshake {
word: AtomicU32,
}
impl Default for Handshake {
fn default() -> Self {
Self::new()
}
}
impl Handshake {
pub(crate) fn new() -> Self {
Self {
word: AtomicU32::new(FUTEX_BLOCKED),
}
}
pub(crate) fn post(&self) {
if self
.word
.compare_exchange(
FUTEX_BLOCKED,
FUTEX_RUNNING,
Ordering::SeqCst,
Ordering::SeqCst,
)
.is_ok()
{
let rc = unsafe {
libc::syscall(
libc::SYS_futex,
self.word.as_ptr(),
FUTEX_WAKE_PRIVATE,
1i32,
core::ptr::null::<libc::timespec>(),
core::ptr::null::<u32>(),
0u32,
)
};
assert!(
rc != -1,
"FUTEX_WAKE_PRIVATE failed: {}",
std::io::Error::last_os_error()
);
}
}
#[allow(dead_code)]
pub(crate) fn wait(&self, timeout: &libc::timespec) -> WaitOutcome {
self.wait_inner(Some(timeout))
}
pub(crate) fn wait_forever(&self) {
let outcome = self.wait_inner(None);
debug_assert_eq!(
outcome,
WaitOutcome::Posted,
"a NULL-timeout wait cannot time out"
);
}
fn wait_inner(&self, timeout: Option<&libc::timespec>) -> WaitOutcome {
loop {
if self
.word
.compare_exchange(
FUTEX_RUNNING,
FUTEX_BLOCKED,
Ordering::SeqCst,
Ordering::SeqCst,
)
.is_ok()
{
return WaitOutcome::Posted;
}
let ts_ptr = match timeout {
Some(ts) => ts as *const libc::timespec,
None => core::ptr::null::<libc::timespec>(),
};
let rc = unsafe {
libc::syscall(
libc::SYS_futex,
self.word.as_ptr(),
FUTEX_WAIT_PRIVATE,
FUTEX_BLOCKED,
ts_ptr,
core::ptr::null::<u32>(),
0u32,
)
};
if rc == -1 {
match std::io::Error::last_os_error().raw_os_error() {
Some(libc::ETIMEDOUT) => return WaitOutcome::TimedOut,
Some(libc::EAGAIN) | Some(libc::EINTR) => {}
other => panic!("FUTEX_WAIT_PRIVATE failed: errno {other:?}"),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn timespec(secs: i64, nanos: i64) -> libc::timespec {
let mut ts: libc::timespec = unsafe { core::mem::zeroed() };
ts.tv_sec = secs as libc::time_t;
ts.tv_nsec = nanos as _;
ts
}
#[test]
fn post_then_wait_consumes_via_fast_path() {
let h = Handshake::new();
h.post();
assert_eq!(h.wait(×pec(5, 0)), WaitOutcome::Posted);
assert_eq!(h.wait(×pec(0, 50_000_000)), WaitOutcome::TimedOut);
}
#[test]
fn wait_times_out_without_post() {
let h = Handshake::new();
assert_eq!(h.wait(×pec(0, 100_000_000)), WaitOutcome::TimedOut);
}
#[test]
fn cross_thread_post_wakes_waiter() {
let h = Handshake::new();
std::thread::scope(|s| {
let waiter = s.spawn(|| h.wait(×pec(5, 0)));
h.post();
assert_eq!(waiter.join().unwrap(), WaitOutcome::Posted);
});
}
#[test]
fn wait_forever_consumes_posted_token() {
let h = Handshake::new();
h.post();
h.wait_forever(); }
}