use std::io;
use std::ptr;
use std::sync::atomic::{AtomicIsize, Ordering};
use std::time::{Duration, SystemTime};
use windows_sys::Win32::Foundation::{FALSE, TRUE};
use windows_sys::Win32::System::Threading::{
CloseThreadpoolTimer, CreateThreadpoolTimer, IsThreadpoolTimerSet, PTP_CALLBACK_INSTANCE,
PTP_TIMER, WaitForThreadpoolTimerCallbacks,
};
use crate::callback_env::CallbackEnviron;
use crate::timer::{absolute_filetime, arm_raw, disarm_raw, millis_u32, relative_filetime};
struct PeriodicContext {
timer: AtomicIsize,
callback: Box<dyn Fn(&PeriodicTick<'_>) + Send + Sync + 'static>,
}
pub struct PeriodicTick<'ctx> {
ctx: &'ctx PeriodicContext,
}
impl std::fmt::Debug for PeriodicTick<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PeriodicTick").finish_non_exhaustive()
}
}
impl PeriodicTick<'_> {
pub fn stop(&self) {
let timer = self.ctx.timer.load(Ordering::Acquire);
debug_assert_ne!(timer, 0, "the timer must be published before callbacks");
unsafe { disarm_raw(timer) };
}
}
unsafe extern "system" fn periodic_trampoline(
_instance: PTP_CALLBACK_INSTANCE,
context: *mut core::ffi::c_void,
_timer: PTP_TIMER,
) {
let ctx = unsafe { &*(context as *const PeriodicContext) };
let tick = PeriodicTick { ctx };
(ctx.callback)(&tick);
}
pub struct ThreadpoolPeriodicTimer {
timer: PTP_TIMER,
period: Duration,
context: *mut PeriodicContext,
}
unsafe impl Send for ThreadpoolPeriodicTimer {}
unsafe impl Sync for ThreadpoolPeriodicTimer {}
const NANOS_PER_MILLI: u32 = 1_000_000;
impl ThreadpoolPeriodicTimer {
pub const MIN_PERIOD: Duration = Duration::from_millis(1);
pub const MAX_PERIOD: Duration = Duration::from_millis(u32::MAX as u64);
pub fn new<F>(
period: Duration,
callback: F,
env: Option<&mut CallbackEnviron>,
) -> io::Result<Self>
where
F: Fn(&PeriodicTick<'_>) + Send + Sync + 'static,
{
if period < Self::MIN_PERIOD {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a ThreadpoolPeriodicTimer needs a period of at least 1ms: the pool takes the period in whole milliseconds, so anything shorter rounds to zero and a zero period means do not repeat; use ThreadpoolTimer for a one-shot",
));
}
if period > Self::MAX_PERIOD {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a ThreadpoolPeriodicTimer period must fit in u32 milliseconds (just under 50 days); a longer one would be capped and tick far more often than asked",
));
}
if !period.subsec_nanos().is_multiple_of(NANOS_PER_MILLI) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a ThreadpoolPeriodicTimer period must be a whole number of milliseconds: the pool truncates the remainder, so the timer would tick sooner than the period it reports",
));
}
let context = Box::into_raw(Box::new(PeriodicContext {
timer: AtomicIsize::new(0),
callback: Box::new(callback),
}));
let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
let timer = unsafe {
CreateThreadpoolTimer(
Some(periodic_trampoline),
context.cast(),
env_ptr.cast_const(),
)
};
if timer == 0 {
let error = io::Error::last_os_error();
unsafe { drop(Box::from_raw(context)) };
return Err(error);
}
unsafe { (*context).timer.store(timer, Ordering::Release) };
Ok(Self {
timer,
period,
context,
})
}
#[must_use]
pub fn period(&self) -> Duration {
self.period
}
pub fn start(&self) {
self.start_after(self.period);
}
pub fn start_after(&self, first_delay: Duration) {
unsafe {
arm_raw(
self.timer,
relative_filetime(first_delay),
millis_u32(self.period),
0,
);
}
}
pub fn start_at(&self, when: SystemTime) {
unsafe {
arm_raw(
self.timer,
absolute_filetime(when),
millis_u32(self.period),
0,
);
}
}
pub fn start_with_window(&self, first_delay: Duration, window: Duration) {
unsafe {
arm_raw(
self.timer,
relative_filetime(first_delay),
millis_u32(self.period),
millis_u32(window),
);
}
}
pub fn stop(&self) {
unsafe { disarm_raw(self.timer) };
}
#[must_use]
pub fn is_running(&self) -> bool {
unsafe { IsThreadpoolTimerSet(self.timer) != 0 }
}
pub fn wait(&self) {
unsafe { WaitForThreadpoolTimerCallbacks(self.timer, FALSE) };
}
pub fn stop_and_drain(&self) {
self.stop();
unsafe { WaitForThreadpoolTimerCallbacks(self.timer, TRUE) };
}
pub(crate) fn into_parts(self) -> (PTP_TIMER, *mut core::ffi::c_void, Duration) {
let this = std::mem::ManuallyDrop::new(self);
(this.timer, this.context.cast(), this.period)
}
pub(crate) unsafe fn drop_context(context: *mut core::ffi::c_void) {
drop(unsafe { Box::from_raw(context.cast::<PeriodicContext>()) });
}
}
impl Drop for ThreadpoolPeriodicTimer {
fn drop(&mut self) {
self.stop_and_drain();
unsafe {
CloseThreadpoolTimer(self.timer);
drop(Box::from_raw(self.context));
}
}
}
impl std::fmt::Debug for ThreadpoolPeriodicTimer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ThreadpoolPeriodicTimer")
.field("period", &self.period)
.field("is_running", &self.is_running())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;