1#![cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
18
19use std::io;
20#[cfg(target_os = "macos")]
21use std::mem::size_of;
22use std::sync::atomic::AtomicU32;
23use std::time::Duration;
24
25pub fn supported() -> bool {
31 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
32 {
33 true
34 }
35 #[cfg(target_os = "macos")]
36 {
37 macos::api().is_some()
38 }
39}
40
41#[cfg(target_os = "linux")]
42pub fn wait_word(
43 word: &AtomicU32,
44 expected: u32,
45) -> io::Result<()> {
46 let result = unsafe {
47 libc::syscall(
48 libc::SYS_futex,
49 word.as_ptr(),
50 libc::FUTEX_WAIT,
51 expected,
52 std::ptr::null::<libc::timespec>(),
53 std::ptr::null::<u32>(),
54 0,
55 )
56 };
57 if result == 0 {
58 return Ok(());
59 }
60
61 let error = io::Error::last_os_error();
62 match error.raw_os_error() {
63 Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(()),
66 _ => Err(error),
67 }
68}
69
70#[cfg(target_os = "linux")]
79pub fn wait_word_timeout(
80 word: &AtomicU32,
81 expected: u32,
82 timeout: Duration,
83) -> io::Result<bool> {
84 let left = libc::timespec {
86 tv_sec: timeout.as_secs().min(i64::MAX as u64) as libc::time_t,
87 tv_nsec: timeout.subsec_nanos() as libc::c_long,
88 };
89 let result = unsafe {
90 libc::syscall(
91 libc::SYS_futex,
92 word.as_ptr(),
93 libc::FUTEX_WAIT,
94 expected,
95 &left as *const libc::timespec,
96 std::ptr::null::<u32>(),
97 0,
98 )
99 };
100 if result == 0 {
101 return Ok(true);
102 }
103
104 let error = io::Error::last_os_error();
105 match error.raw_os_error() {
106 Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(true),
107 Some(libc::ETIMEDOUT) => Ok(false),
108 _ => Err(error),
109 }
110}
111
112#[cfg(target_os = "linux")]
113pub fn wake_word_one(word: &AtomicU32) -> io::Result<()> {
114 let result = unsafe {
115 libc::syscall(
116 libc::SYS_futex,
117 word.as_ptr(),
118 libc::FUTEX_WAKE,
119 1,
120 std::ptr::null::<libc::timespec>(),
121 std::ptr::null::<u32>(),
122 0,
123 )
124 };
125 if result >= 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
126}
127
128#[cfg(target_os = "linux")]
129pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
130 let result = unsafe {
131 libc::syscall(
132 libc::SYS_futex,
133 word.as_ptr(),
134 libc::FUTEX_WAKE,
135 i32::MAX,
136 std::ptr::null::<libc::timespec>(),
137 std::ptr::null::<u32>(),
138 0,
139 )
140 };
141 if result >= 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
142}
143
144#[cfg(target_os = "freebsd")]
145pub fn wait_word(
146 word: &AtomicU32,
147 expected: u32,
148) -> io::Result<()> {
149 let result = unsafe {
150 libc::_umtx_op(
151 word.as_ptr().cast(),
152 libc::UMTX_OP_WAIT_UINT,
153 expected as libc::c_ulong,
154 std::ptr::null_mut(),
155 std::ptr::null_mut(),
156 )
157 };
158 if result == 0 {
159 return Ok(());
160 }
161
162 let error = io::Error::last_os_error();
163 match error.raw_os_error() {
164 Some(libc::EINTR) => Ok(()),
167 _ => Err(error),
168 }
169}
170
171#[cfg(target_os = "freebsd")]
180pub fn wait_word_timeout(
181 word: &AtomicU32,
182 expected: u32,
183 timeout: Duration,
184) -> io::Result<bool> {
185 let left = libc::timespec {
189 tv_sec: timeout.as_secs().min(i64::MAX as u64) as libc::time_t,
190 tv_nsec: timeout.subsec_nanos() as libc::c_long,
191 };
192 let result = unsafe {
193 libc::_umtx_op(
194 word.as_ptr().cast(),
195 libc::UMTX_OP_WAIT_UINT,
196 expected as libc::c_ulong,
197 size_of::<libc::timespec>() as *mut libc::c_void,
198 &left as *const libc::timespec as *mut libc::c_void,
199 )
200 };
201 if result == 0 {
202 return Ok(true);
203 }
204
205 let error = io::Error::last_os_error();
206 match error.raw_os_error() {
207 Some(libc::EINTR) => Ok(true),
208 Some(libc::ETIMEDOUT) => Ok(false),
209 _ => Err(error),
210 }
211}
212
213#[cfg(target_os = "freebsd")]
214pub fn wake_word_one(word: &AtomicU32) -> io::Result<()> {
215 let result = unsafe {
216 libc::_umtx_op(
217 word.as_ptr().cast(),
218 libc::UMTX_OP_WAKE,
219 1,
220 std::ptr::null_mut(),
221 std::ptr::null_mut(),
222 )
223 };
224 if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
225}
226
227#[cfg(target_os = "freebsd")]
228pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
229 let result = unsafe {
230 libc::_umtx_op(
231 word.as_ptr().cast(),
232 libc::UMTX_OP_WAKE,
233 i32::MAX as libc::c_ulong,
234 std::ptr::null_mut(),
235 std::ptr::null_mut(),
236 )
237 };
238 if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) }
239}
240
241#[cfg(target_os = "macos")]
242pub fn wait_word(
243 word: &AtomicU32,
244 expected: u32,
245) -> io::Result<()> {
246 let api = macos::api().ok_or_else(|| {
247 io::Error::new(
248 io::ErrorKind::Unsupported,
249 "macOS shared address waits unavailable",
250 )
251 })?;
252 debug_assert_eq!(
253 (word.as_ptr() as usize) % size_of::<u32>(),
254 0,
255 "shared wait word must be naturally aligned"
256 );
257 let result = unsafe {
260 (api.wait)(
261 word.as_ptr().cast(),
262 u64::from(expected),
263 size_of::<u32>(),
264 macos::SHARED,
265 )
266 };
267 if result >= 0 {
268 return Ok(());
269 }
270 let error = io::Error::last_os_error();
271 match error.raw_os_error() {
272 Some(libc::EINTR) => Ok(()),
273 _ => Err(error),
274 }
275}
276
277#[cfg(target_os = "macos")]
286pub fn wait_word_timeout(
287 word: &AtomicU32,
288 expected: u32,
289 timeout: Duration,
290) -> io::Result<bool> {
291 let api = macos::api().ok_or_else(|| {
292 io::Error::new(
293 io::ErrorKind::Unsupported,
294 "macOS shared address waits unavailable",
295 )
296 })?;
297 let nanos = timeout.as_nanos().min(u128::from(u64::MAX)) as u64;
298 let result = unsafe {
300 (api.wait_timeout)(
301 word.as_ptr().cast(),
302 u64::from(expected),
303 size_of::<u32>(),
304 macos::SHARED,
305 macos::MACH_ABSOLUTE_TIME,
306 nanos,
307 )
308 };
309 if result >= 0 {
310 return Ok(true);
311 }
312 let error = io::Error::last_os_error();
313 match error.raw_os_error() {
314 Some(libc::EINTR) => Ok(true),
315 Some(libc::ETIMEDOUT) => Ok(false),
316 _ => Err(error),
317 }
318}
319
320#[cfg(target_os = "macos")]
321pub fn wake_word_one(word: &AtomicU32) -> io::Result<()> {
322 wake_macos(word, false)
323}
324
325#[cfg(target_os = "macos")]
326pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
327 wake_macos(word, true)
328}
329
330#[cfg(target_os = "macos")]
331fn wake_macos(
332 word: &AtomicU32,
333 all: bool,
334) -> io::Result<()> {
335 debug_assert_eq!(
336 (word.as_ptr() as usize) % size_of::<u32>(),
337 0,
338 "shared wake word must be naturally aligned"
339 );
340 let Some(api) = macos::api() else {
343 return Ok(());
344 };
345 loop {
346 let wake = if all { api.wake_all } else { api.wake_one };
347 let result = unsafe {
348 wake(word.as_ptr().cast(), size_of::<u32>(), macos::SHARED)
349 };
350 if result >= 0 {
351 return Ok(());
352 }
353 let error = io::Error::last_os_error();
354 match error.raw_os_error() {
355 Some(libc::ENOENT) => return Ok(()),
358 Some(libc::EINTR) => continue,
359 _ => return Err(error),
360 }
361 }
362}
363
364#[cfg(target_os = "macos")]
365pub(crate) mod macos {
366 use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
367
368 pub(super) const SHARED: u32 = 1;
371
372 pub(super) const MACH_ABSOLUTE_TIME: u32 = 32;
375
376 type Wait =
377 unsafe extern "C" fn(*mut libc::c_void, u64, usize, u32) -> libc::c_int;
378 type WaitTimeout = unsafe extern "C" fn(
379 *mut libc::c_void,
380 u64,
381 usize,
382 u32,
383 u32,
384 u64,
385 ) -> libc::c_int;
386 type Wake =
387 unsafe extern "C" fn(*mut libc::c_void, usize, u32) -> libc::c_int;
388
389 #[derive(Clone, Copy)]
390 pub(crate) struct Api {
391 pub(super) wait: Wait,
392 pub(super) wait_timeout: WaitTimeout,
393 pub(super) wake_one: Wake,
394 pub(super) wake_all: Wake,
395 }
396
397 const UNRESOLVED: u8 = 0;
398 const UNAVAILABLE: u8 = 1;
399 const READY: u8 = 2;
400
401 static STATE: AtomicU8 = AtomicU8::new(UNRESOLVED);
402 static WAIT: AtomicUsize = AtomicUsize::new(0);
403 static WAIT_TIMEOUT: AtomicUsize = AtomicUsize::new(0);
404 static WAKE_ONE: AtomicUsize = AtomicUsize::new(0);
405 static WAKE_ALL: AtomicUsize = AtomicUsize::new(0);
406
407 pub(crate) fn api() -> Option<Api> {
423 match STATE.load(Ordering::Acquire) {
424 READY => Some(load()),
425 UNAVAILABLE => None,
426 _ => resolve(),
427 }
428 }
429
430 fn load() -> Api {
431 unsafe {
432 Api {
433 wait: std::mem::transmute::<usize, Wait>(
434 WAIT.load(Ordering::Acquire),
435 ),
436 wait_timeout: std::mem::transmute::<usize, WaitTimeout>(
437 WAIT_TIMEOUT.load(Ordering::Acquire),
438 ),
439 wake_one: std::mem::transmute::<usize, Wake>(
440 WAKE_ONE.load(Ordering::Acquire),
441 ),
442 wake_all: std::mem::transmute::<usize, Wake>(
443 WAKE_ALL.load(Ordering::Acquire),
444 ),
445 }
446 }
447 }
448
449 fn resolve() -> Option<Api> {
450 let wait = unsafe {
451 libc::dlsym(libc::RTLD_DEFAULT, c"os_sync_wait_on_address".as_ptr())
452 };
453 let wait_timeout = unsafe {
455 libc::dlsym(
456 libc::RTLD_DEFAULT,
457 c"os_sync_wait_on_address_with_timeout".as_ptr(),
458 )
459 };
460 let wake_one = unsafe {
461 libc::dlsym(
462 libc::RTLD_DEFAULT,
463 c"os_sync_wake_by_address_any".as_ptr(),
464 )
465 };
466 let wake_all = unsafe {
467 libc::dlsym(
468 libc::RTLD_DEFAULT,
469 c"os_sync_wake_by_address_all".as_ptr(),
470 )
471 };
472 if wait.is_null()
473 || wait_timeout.is_null()
474 || wake_one.is_null()
475 || wake_all.is_null()
476 {
477 STATE.store(UNAVAILABLE, Ordering::Release);
478 return None;
479 }
480 WAIT.store(wait as usize, Ordering::Release);
482 WAIT_TIMEOUT.store(wait_timeout as usize, Ordering::Release);
483 WAKE_ONE.store(wake_one as usize, Ordering::Release);
484 WAKE_ALL.store(wake_all as usize, Ordering::Release);
485 STATE.store(READY, Ordering::Release);
486 Some(load())
487 }
488}
489
490#[cfg(test)]
491mod timeout_tests {
492 use std::sync::Arc;
493 use std::sync::atomic::Ordering;
494 use std::time::Instant;
495
496 use super::*;
497
498 #[test]
502 fn a_timeout_is_a_timeout() {
503 if !supported() {
504 return;
505 }
506 let word = AtomicU32::new(7);
507 let started = Instant::now();
508 assert!(
509 !wait_word_timeout(&word, 7, Duration::from_millis(200))
510 .expect("wait")
511 );
512 let waited = started.elapsed();
513 assert!(
514 waited >= Duration::from_millis(150),
515 "returned after {waited:?}"
516 );
517 assert!(waited < Duration::from_secs(2), "returned after {waited:?}");
518 }
519
520 #[test]
521 fn a_wake_beats_the_timeout() {
522 if !supported() {
523 return;
524 }
525 let word = Arc::new(AtomicU32::new(0));
526 let waker = Arc::clone(&word);
527 std::thread::spawn(move || {
528 std::thread::sleep(Duration::from_millis(50));
529 waker.store(1, Ordering::SeqCst);
530 let _ = wake_word(&waker);
531 });
532 let started = Instant::now();
533 assert!(
534 wait_word_timeout(&word, 0, Duration::from_secs(10)).expect("wait")
535 );
536 assert!(started.elapsed() < Duration::from_secs(5));
537 }
538
539 #[test]
540 fn wake_one_releases_only_one_parked_waiter() {
541 if !supported() {
542 return;
543 }
544 let word = Arc::new(AtomicU32::new(0));
545 let (sent, received) = std::sync::mpsc::channel();
546 let waiters = (0..2)
547 .map(|_| {
548 let word = Arc::clone(&word);
549 let sent = sent.clone();
550 std::thread::spawn(move || {
551 let outcome =
552 wait_word_timeout(&word, 0, Duration::from_millis(500))
553 .expect("wait");
554 sent.send(outcome).unwrap();
555 })
556 })
557 .collect::<Vec<_>>();
558
559 std::thread::sleep(Duration::from_millis(100));
560 word.store(1, Ordering::SeqCst);
561 wake_word_one(&word).expect("wake one");
562
563 assert!(received.recv_timeout(Duration::from_millis(200)).unwrap());
564 assert!(received.recv_timeout(Duration::from_millis(100)).is_err());
565 assert!(!received.recv_timeout(Duration::from_millis(400)).unwrap());
566 for waiter in waiters {
567 waiter.join().unwrap();
568 }
569 }
570
571 #[test]
572 fn a_word_that_already_moved_does_not_park_at_all() {
573 if !supported() {
574 return;
575 }
576 let word = AtomicU32::new(3);
577 let started = Instant::now();
578 assert!(
579 wait_word_timeout(&word, 9, Duration::from_secs(30)).expect("wait")
580 );
581 assert!(started.elapsed() < Duration::from_secs(1));
582 }
583}