425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
use std::mem;
|
|
use std::os::raw::{c_int, c_ulong};
|
|
use std::ptr;
|
|
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
use std::thread::JoinHandle;
|
|
use std::time::Duration;
|
|
|
|
#[repr(C)]
|
|
struct Timespec {
|
|
tv_sec: i64,
|
|
tv_nsec: i64,
|
|
}
|
|
|
|
unsafe extern "C" {
|
|
fn clock_gettime(clock_id: c_int, tp: *mut Timespec) -> c_int;
|
|
}
|
|
|
|
const CLOCK_MONOTONIC: c_int = 1;
|
|
|
|
struct TimerEntry {
|
|
generation: AtomicU64,
|
|
active: AtomicBool,
|
|
function: AtomicPtr<()>,
|
|
data: AtomicPtr<u8>,
|
|
handles: Mutex<Vec<JoinHandle<()>>>,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct TimerList {
|
|
expires: AtomicU64,
|
|
function: AtomicPtr<()>,
|
|
data: AtomicPtr<u8>,
|
|
active: AtomicBool,
|
|
}
|
|
|
|
fn timer_entries() -> &'static Mutex<HashMap<usize, Arc<TimerEntry>>> {
|
|
static TIMER_ENTRIES: OnceLock<Mutex<HashMap<usize, Arc<TimerEntry>>>> = OnceLock::new();
|
|
TIMER_ENTRIES.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
fn current_jiffies() -> u64 {
|
|
let mut ts = Timespec {
|
|
tv_sec: 0,
|
|
tv_nsec: 0,
|
|
};
|
|
let result = unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) };
|
|
if result != 0 || ts.tv_sec < 0 || ts.tv_nsec < 0 {
|
|
return 0;
|
|
}
|
|
|
|
(ts.tv_sec as u64)
|
|
.saturating_mul(1_000)
|
|
.saturating_add((ts.tv_nsec as u64) / 1_000_000)
|
|
}
|
|
|
|
fn lock_timer_entries() -> std::sync::MutexGuard<'static, HashMap<usize, Arc<TimerEntry>>> {
|
|
match timer_entries().lock() {
|
|
Ok(entries) => entries,
|
|
Err(e) => e.into_inner(),
|
|
}
|
|
}
|
|
|
|
fn lock_timer_handles(entry: &TimerEntry) -> std::sync::MutexGuard<'_, Vec<JoinHandle<()>>> {
|
|
match entry.handles.lock() {
|
|
Ok(handles) => handles,
|
|
Err(e) => e.into_inner(),
|
|
}
|
|
}
|
|
|
|
fn timer_entry(timer: *mut TimerList) -> Arc<TimerEntry> {
|
|
let mut entries = lock_timer_entries();
|
|
entries
|
|
.entry(timer as usize)
|
|
.or_insert_with(|| {
|
|
Arc::new(TimerEntry {
|
|
generation: AtomicU64::new(0),
|
|
active: AtomicBool::new(false),
|
|
function: AtomicPtr::new(ptr::null_mut()),
|
|
data: AtomicPtr::new(ptr::null_mut()),
|
|
handles: Mutex::new(Vec::new()),
|
|
})
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
fn reset_timer_entry(timer: *mut TimerList, function: *mut (), data: *mut u8) {
|
|
let mut entries = lock_timer_entries();
|
|
if let Some(entry) = entries.get(&(timer as usize)) {
|
|
entry.active.store(false, Ordering::Release);
|
|
entry.generation.fetch_add(1, Ordering::AcqRel);
|
|
}
|
|
entries.insert(
|
|
timer as usize,
|
|
Arc::new(TimerEntry {
|
|
generation: AtomicU64::new(0),
|
|
active: AtomicBool::new(false),
|
|
function: AtomicPtr::new(function),
|
|
data: AtomicPtr::new(data),
|
|
handles: Mutex::new(Vec::new()),
|
|
}),
|
|
);
|
|
}
|
|
|
|
fn join_all_handles(entry: &TimerEntry) {
|
|
let handles = {
|
|
let mut guard = lock_timer_handles(entry);
|
|
mem::take(&mut *guard)
|
|
};
|
|
|
|
for handle in handles {
|
|
let _ = handle.join();
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn setup_timer(
|
|
timer: *mut TimerList,
|
|
function: extern "C" fn(c_ulong),
|
|
data: c_ulong,
|
|
) {
|
|
if timer.is_null() {
|
|
return;
|
|
}
|
|
|
|
let function_ptr = function as usize as *mut ();
|
|
unsafe {
|
|
ptr::write(
|
|
timer,
|
|
TimerList {
|
|
expires: AtomicU64::new(0),
|
|
function: AtomicPtr::new(function_ptr),
|
|
data: AtomicPtr::new(data as usize as *mut u8),
|
|
active: AtomicBool::new(false),
|
|
},
|
|
);
|
|
}
|
|
|
|
reset_timer_entry(timer, function_ptr, data as usize as *mut u8);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn mod_timer(timer: *mut TimerList, expires: u64) -> i32 {
|
|
if timer.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
let timer_ref = unsafe { &*timer };
|
|
let entry = timer_entry(timer);
|
|
entry.function.store(
|
|
timer_ref.function.load(Ordering::Acquire),
|
|
Ordering::Release,
|
|
);
|
|
entry
|
|
.data
|
|
.store(timer_ref.data.load(Ordering::Acquire), Ordering::Release);
|
|
|
|
let was_active = entry.active.swap(true, Ordering::AcqRel);
|
|
timer_ref.active.store(true, Ordering::Release);
|
|
timer_ref.expires.store(expires, Ordering::Release);
|
|
let generation = entry
|
|
.generation
|
|
.fetch_add(1, Ordering::AcqRel)
|
|
.wrapping_add(1);
|
|
|
|
let delay = expires.saturating_sub(current_jiffies());
|
|
let function_addr = entry.function.load(Ordering::Acquire) as usize;
|
|
let data_addr = entry.data.load(Ordering::Acquire) as usize;
|
|
let entry_for_thread = entry.clone();
|
|
let handle = std::thread::spawn(move || {
|
|
let start = std::time::Instant::now();
|
|
let total = Duration::from_millis(delay);
|
|
loop {
|
|
let elapsed = start.elapsed();
|
|
if elapsed >= total {
|
|
break;
|
|
}
|
|
if !entry_for_thread.active.load(Ordering::Acquire) {
|
|
return;
|
|
}
|
|
if entry_for_thread.generation.load(Ordering::Acquire) != generation {
|
|
return;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
|
|
if !entry_for_thread.active.load(Ordering::Acquire) {
|
|
return;
|
|
}
|
|
|
|
if entry_for_thread.generation.load(Ordering::Acquire) != generation {
|
|
return;
|
|
}
|
|
|
|
if function_addr == 0 {
|
|
entry_for_thread.active.store(false, Ordering::Release);
|
|
return;
|
|
}
|
|
|
|
let function =
|
|
unsafe { std::mem::transmute::<usize, extern "C" fn(c_ulong)>(function_addr) };
|
|
function(data_addr as c_ulong);
|
|
|
|
if entry_for_thread.generation.load(Ordering::Acquire) == generation {
|
|
entry_for_thread.active.store(false, Ordering::Release);
|
|
}
|
|
});
|
|
|
|
lock_timer_handles(&entry).push(handle);
|
|
|
|
if was_active {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn del_timer(timer: *mut TimerList) -> i32 {
|
|
if timer.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
let timer_ref = unsafe { &*timer };
|
|
let entry = timer_entry(timer);
|
|
let was_active = entry.active.swap(false, Ordering::AcqRel);
|
|
entry.generation.fetch_add(1, Ordering::AcqRel);
|
|
timer_ref.active.store(false, Ordering::Release);
|
|
|
|
if was_active {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn del_timer_sync(timer: *mut TimerList) -> i32 {
|
|
if timer.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
let timer_ref = unsafe { &*timer };
|
|
let entry = timer_entry(timer);
|
|
let was_active = entry.active.swap(false, Ordering::AcqRel);
|
|
entry.generation.fetch_add(1, Ordering::AcqRel);
|
|
timer_ref.active.store(false, Ordering::Release);
|
|
join_all_handles(&entry);
|
|
|
|
if was_active {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn timer_pending(timer: *const TimerList) -> i32 {
|
|
if timer.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
let entries = lock_timer_entries();
|
|
match entries.get(&(timer as usize)) {
|
|
Some(entry) if entry.active.load(Ordering::Acquire) => 1,
|
|
Some(_) => 0,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::atomic::AtomicUsize;
|
|
|
|
extern "C" fn test_timer_callback(_data: c_ulong) {}
|
|
|
|
#[test]
|
|
fn setup_timer_initializes_struct() {
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), test_timer_callback, 42);
|
|
|
|
let timer_ref = unsafe { &*timer.as_ptr() };
|
|
assert_eq!(timer_ref.expires.load(Ordering::Acquire), 0);
|
|
assert!(!timer_ref.active.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[test]
|
|
fn setup_timer_null_pointer_is_safe() {
|
|
setup_timer(std::ptr::null_mut(), test_timer_callback, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mod_timer_returns_was_active() {
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
|
|
|
|
let was_active = mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
|
|
assert_eq!(
|
|
was_active, 0,
|
|
"first mod_timer should return 0 (was inactive)"
|
|
);
|
|
|
|
let was_active = mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
|
|
assert_eq!(
|
|
was_active, 1,
|
|
"second mod_timer should return 1 (was active)"
|
|
);
|
|
|
|
del_timer_sync(timer.as_mut_ptr());
|
|
}
|
|
|
|
#[test]
|
|
fn mod_timer_null_pointer_returns_zero() {
|
|
assert_eq!(mod_timer(std::ptr::null_mut(), 1000), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn del_timer_cancels_active_timer() {
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
|
|
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
|
|
|
|
let result = del_timer_sync(timer.as_mut_ptr());
|
|
assert_eq!(result, 1, "del_timer should return 1 for active timer");
|
|
}
|
|
|
|
#[test]
|
|
fn del_timer_null_pointer_returns_zero() {
|
|
assert_eq!(del_timer(std::ptr::null_mut()), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn del_timer_sync_cancels_long_timer() {
|
|
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
extern "C" fn counting_callback(_data: c_ulong) {
|
|
COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), counting_callback, 0);
|
|
|
|
COUNTER.store(0, Ordering::Relaxed);
|
|
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
|
|
|
|
let result = del_timer_sync(timer.as_mut_ptr());
|
|
assert_eq!(result, 1, "del_timer_sync should return 1 for active timer");
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
assert_eq!(
|
|
COUNTER.load(Ordering::Relaxed),
|
|
0,
|
|
"callback should not have fired after cancel"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn del_timer_returns_zero_for_inactive() {
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
|
|
|
|
assert_eq!(
|
|
del_timer(timer.as_mut_ptr()),
|
|
0,
|
|
"del_timer on inactive timer should return 0"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn timer_pending_null_returns_zero() {
|
|
assert_eq!(timer_pending(std::ptr::null()), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn timer_pending_reflects_state() {
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
|
|
|
|
assert_eq!(
|
|
timer_pending(timer.as_ptr()),
|
|
0,
|
|
"new timer should not be pending"
|
|
);
|
|
|
|
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
|
|
assert_eq!(
|
|
timer_pending(timer.as_ptr()),
|
|
1,
|
|
"armed timer should be pending"
|
|
);
|
|
|
|
del_timer_sync(timer.as_mut_ptr());
|
|
assert_eq!(
|
|
timer_pending(timer.as_ptr()),
|
|
0,
|
|
"cancelled timer should not be pending"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mod_timer_fires_callback() {
|
|
static FIRE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
extern "C" fn fire_callback(_data: c_ulong) {
|
|
FIRE_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
|
|
setup_timer(timer.as_mut_ptr(), fire_callback, 0);
|
|
|
|
FIRE_COUNT.store(0, Ordering::Relaxed);
|
|
mod_timer(timer.as_mut_ptr(), current_jiffies());
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
assert!(
|
|
FIRE_COUNT.load(Ordering::Relaxed) >= 1,
|
|
"callback should have fired"
|
|
);
|
|
|
|
del_timer_sync(timer.as_mut_ptr());
|
|
}
|
|
}
|