use std::sync::OnceLock;
use std::thread::Thread;
use std::time::Duration;
pub(super) struct Bell {
thread: OnceLock<Thread>,
#[cfg(windows)]
handles: Option<windows::Handles>,
#[cfg(target_vendor = "apple")]
handles: Option<apple::Handles>,
#[cfg(test)]
rings: core::sync::atomic::AtomicUsize,
}
impl Bell {
pub(super) fn new() -> Self {
Self {
thread: OnceLock::new(),
#[cfg(windows)]
handles: windows::Handles::new(),
#[cfg(target_vendor = "apple")]
handles: apple::Handles::new(),
#[cfg(test)]
rings: core::sync::atomic::AtomicUsize::new(0),
}
}
#[cfg(test)]
pub(super) fn rings(&self) -> usize {
self.rings.load(core::sync::atomic::Ordering::SeqCst)
}
pub(super) fn bind(&self) {
let _ = self.thread.set(std::thread::current());
}
pub(super) fn wait(&self) {
#[cfg(any(windows, target_vendor = "apple"))]
if self.handles.as_ref().is_some_and(|h| h.wait(None)) {
return;
}
std::thread::park();
}
pub(super) fn wait_for(&self, left: Duration) {
#[cfg(any(windows, target_vendor = "apple"))]
if self.handles.as_ref().is_some_and(|h| h.wait(Some(left))) {
return;
}
std::thread::park_timeout(left);
}
pub(super) fn ring(&self) {
#[cfg(test)]
self.rings
.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
#[cfg(any(windows, target_vendor = "apple"))]
if let Some(handles) = &self.handles {
handles.ring();
}
if let Some(thread) = self.thread.get() {
thread.unpark();
}
}
}
#[cfg(windows)]
mod windows {
use std::time::Duration;
use crate::syscore::windows::{
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CloseHandle, CreateEventW, CreateWaitableTimerExW,
HANDLE, INFINITE, SetEvent, SetWaitableTimer, TIMER_ALL_ACCESS, WAIT_OBJECT_0,
WaitForMultipleObjects, WaitForSingleObject,
};
pub(super) struct Handles {
event: HANDLE,
timer: HANDLE,
}
unsafe impl Send for Handles {}
unsafe impl Sync for Handles {}
impl Handles {
pub(super) fn new() -> Option<Self> {
unsafe {
let event = CreateEventW(core::ptr::null_mut(), 0, 0, core::ptr::null());
if event.is_null() {
return None;
}
let timer = CreateWaitableTimerExW(
core::ptr::null_mut(),
core::ptr::null(),
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_ALL_ACCESS,
);
if timer.is_null() {
CloseHandle(event);
return None;
}
Some(Self { event, timer })
}
}
pub(super) fn wait(&self, left: Option<Duration>) -> bool {
unsafe {
let Some(left) = left else {
return WaitForSingleObject(self.event, INFINITE) == WAIT_OBJECT_0;
};
let due = -(i64::try_from(left.as_nanos().div_ceil(100)).unwrap_or(i64::MAX));
if SetWaitableTimer(
self.timer,
&due,
0,
core::ptr::null(),
core::ptr::null_mut(),
0,
) == 0
{
return false;
}
let both = [self.event, self.timer];
WaitForMultipleObjects(2, both.as_ptr(), 0, INFINITE) < WAIT_OBJECT_0 + 2
}
}
pub(super) fn ring(&self) {
unsafe {
SetEvent(self.event);
}
}
}
impl Drop for Handles {
fn drop(&mut self) {
unsafe {
CloseHandle(self.timer);
CloseHandle(self.event);
}
}
}
}
#[cfg(target_vendor = "apple")]
mod apple {
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::time::Duration;
const IDENT: libc::uintptr_t = 0;
pub(super) struct Handles {
queue: OwnedFd,
}
fn change(filter: i16, flags: u16, fflags: u32, data: isize) -> libc::kevent {
let mut event: libc::kevent = unsafe { core::mem::zeroed() };
event.ident = IDENT;
event.filter = filter;
event.flags = flags;
event.fflags = fflags;
event.data = data;
event
}
impl Handles {
pub(super) fn new() -> Option<Self> {
let raw = unsafe { libc::kqueue() };
if raw < 0 {
return None;
}
let queue = unsafe { OwnedFd::from_raw_fd(raw) };
unsafe { libc::fcntl(queue.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) };
let handles = Self { queue };
let user = change(libc::EVFILT_USER, libc::EV_ADD | libc::EV_CLEAR, 0, 0);
handles.submit(&user, None).then_some(handles)
}
fn submit(&self, change: &libc::kevent, out: Option<&mut libc::kevent>) -> bool {
let (events, n) = match out {
Some(event) => (core::ptr::from_mut(event), 1),
None => (core::ptr::null_mut(), 0),
};
let ret = unsafe {
libc::kevent(
self.queue.as_raw_fd(),
change,
1,
events,
n,
core::ptr::null(),
)
};
ret >= 0
}
pub(super) fn wait(&self, left: Option<Duration>) -> bool {
let mut out: libc::kevent = unsafe { core::mem::zeroed() };
let Some(left) = left else {
let ret = unsafe {
libc::kevent(
self.queue.as_raw_fd(),
core::ptr::null(),
0,
&mut out,
1,
core::ptr::null(),
)
};
return ret >= 0;
};
let nanos = isize::try_from(left.as_nanos()).unwrap_or(isize::MAX);
let timer = change(
libc::EVFILT_TIMER,
libc::EV_ADD | libc::EV_ONESHOT,
libc::NOTE_NSECONDS | libc::NOTE_CRITICAL,
nanos,
);
self.submit(&timer, Some(&mut out))
}
pub(super) fn ring(&self) {
let _ = self.submit(&change(libc::EVFILT_USER, 0, libc::NOTE_TRIGGER, 0), None);
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use std::time::{Duration, Instant};
use super::Bell;
#[test]
fn a_ring_before_the_wait_ends_it_at_once() {
let bell = Bell::new();
bell.bind();
bell.ring();
let start = Instant::now();
bell.wait_for(Duration::from_secs(20));
assert!(
start.elapsed() < Duration::from_secs(10),
"the ring was lost"
);
}
#[test]
fn a_timed_wait_lasts_at_least_its_time() {
let bell = Bell::new();
bell.bind();
for micros in [500, 2500, 10_000] {
let left = Duration::from_micros(micros);
let start = Instant::now();
while start.elapsed() < left {
bell.wait_for(left.saturating_sub(start.elapsed()));
}
assert!(start.elapsed() >= left);
}
}
#[cfg(target_vendor = "apple")]
#[test]
fn the_bell_times_its_waits_with_a_critical_timer_on_apple() {
let bell = Bell::new();
assert!(bell.handles.is_some(), "the bell parks");
bell.bind();
let left = Duration::from_micros(2500);
let mut took: Vec<Duration> = (0..64)
.map(|_| {
let start = Instant::now();
bell.wait_for(left);
start.elapsed()
})
.collect();
took.sort_unstable();
assert!(
took[took.len() / 2] < Duration::from_millis(10),
"a 2.5 ms wait took {:?} at the median",
took[took.len() / 2]
);
}
}