11use super :: Mutex ;
22use crate :: cell:: UnsafeCell ;
33use crate :: pin:: Pin ;
4- #[ cfg( not( target_os = "nto" ) ) ]
5- use crate :: sys:: pal:: time:: TIMESPEC_MAX ;
6- #[ cfg( target_os = "nto" ) ]
7- use crate :: sys:: pal:: time:: TIMESPEC_MAX_CAPPED ;
8- use crate :: sys:: pal:: time:: Timespec ;
94use crate :: time:: Duration ;
105
116pub struct Condvar {
@@ -47,27 +42,29 @@ impl Condvar {
4742 let r = unsafe { libc:: pthread_cond_wait ( self . raw ( ) , mutex. raw ( ) ) } ;
4843 debug_assert_eq ! ( r, 0 ) ;
4944 }
45+ }
5046
47+ #[ cfg( not( target_vendor = "apple" ) ) ]
48+ impl Condvar {
5149 /// # Safety
5250 /// * `init` must have been called on this instance.
5351 /// * `mutex` must be locked by the current thread.
5452 /// * This condition variable may only be used with the same mutex.
5553 pub unsafe fn wait_timeout ( & self , mutex : Pin < & Mutex > , dur : Duration ) -> bool {
54+ #[ cfg( not( target_os = "nto" ) ) ]
55+ use crate :: sys:: pal:: time:: TIMESPEC_MAX ;
56+ #[ cfg( target_os = "nto" ) ]
57+ use crate :: sys:: pal:: time:: TIMESPEC_MAX_CAPPED ;
58+ use crate :: sys:: pal:: time:: Timespec ;
59+
5660 let mutex = mutex. raw ( ) ;
5761
58- // OSX implementation of `pthread_cond_timedwait` is buggy
59- // with super long durations. When duration is greater than
60- // 0x100_0000_0000_0000 seconds, `pthread_cond_timedwait`
61- // in macOS Sierra returns error 316.
62- //
63- // This program demonstrates the issue:
64- // https://gist.github.com/stepancheg/198db4623a20aad2ad7cddb8fda4a63c
65- //
66- // To work around this issue, the timeout is clamped to 1000 years.
67- //
68- // Cygwin implementation is based on NT API and a super large timeout
69- // makes the syscall block forever.
70- #[ cfg( any( target_vendor = "apple" , target_os = "cygwin" ) ) ]
62+ // Cygwin's implementation is based on the NT API, which measures time
63+ // in units of 100 ns. Unfortunately, Cygwin does not properly guard
64+ // against overflow when converting the time, hence we clamp the interval
65+ // to 1000 years, which will only become a problem in around 27000 years,
66+ // when the next rollover is less than 1000 years away...
67+ #[ cfg( target_os = "cygwin" ) ]
7168 let dur = Duration :: min ( dur, Duration :: from_secs ( 1000 * 365 * 86400 ) ) ;
7269
7370 let timeout = Timespec :: now ( Self :: CLOCK ) . checked_add_duration ( & dur) ;
@@ -84,6 +81,57 @@ impl Condvar {
8481 }
8582}
8683
84+ // Apple platforms (since macOS version 10.4 and iOS version 2.0) have
85+ // `pthread_cond_timedwait_relative_np`, a non-standard extension that
86+ // measures timeouts based on the monotonic clock and is thus resilient
87+ // against wall-clock changes.
88+ #[ cfg( target_vendor = "apple" ) ]
89+ impl Condvar {
90+ /// # Safety
91+ /// * `init` must have been called on this instance.
92+ /// * `mutex` must be locked by the current thread.
93+ /// * This condition variable may only be used with the same mutex.
94+ pub unsafe fn wait_timeout ( & self , mutex : Pin < & Mutex > , dur : Duration ) -> bool {
95+ let mutex = mutex. raw ( ) ;
96+
97+ // The macOS implementation of `pthread_cond_timedwait` internally
98+ // converts the timeout passed to `pthread_cond_timedwait_relative_np`
99+ // to nanoseconds. Unfortunately, the "psynch" variant of condvars does
100+ // not guard against overflow during the conversion[^1], which means
101+ // that `pthread_cond_timedwait_relative_np` will return `ETIMEDOUT`
102+ // much earlier than expected if the relative timeout is longer than
103+ // `u64::MAX` nanoseconds.
104+ //
105+ // This can be observed even on newer platforms (by setting the environment
106+ // variable PTHREAD_MUTEX_USE_ULOCK to a value other than "1") by calling e.g.
107+ // ```
108+ // condvar.wait_timeout(..., Duration::from_secs(u64::MAX.div_ceil(1_000_000_000));
109+ // ```
110+ // (see #37440, especially
111+ // https://github.com/rust-lang/rust/issues/37440#issuecomment-3285958326).
112+ //
113+ // To work around this issue, always clamp the timeout to u64::MAX nanoseconds,
114+ // even if the "ulock" variant is used (which does guard against overflow).
115+ //
116+ // [^1]: https://github.com/apple-oss-distributions/libpthread/blob/1ebf56b3a702df53213c2996e5e128a535d2577e/kern/kern_synch.c#L1269
117+ const MAX_DURATION : Duration = Duration :: from_nanos ( u64:: MAX ) ;
118+
119+ let ( dur, clamped) = if dur <= MAX_DURATION { ( dur, false ) } else { ( MAX_DURATION , true ) } ;
120+
121+ let timeout = libc:: timespec {
122+ // This cannot overflow because of the clamping above.
123+ tv_sec : dur. as_secs ( ) as i64 ,
124+ tv_nsec : dur. subsec_nanos ( ) as i64 ,
125+ } ;
126+
127+ let r = unsafe { libc:: pthread_cond_timedwait_relative_np ( self . raw ( ) , mutex, & timeout) } ;
128+ assert ! ( r == libc:: ETIMEDOUT || r == 0 ) ;
129+ // Report clamping as a spurious wakeup. Who knows, maybe some
130+ // interstellar space probe will rely on this ;-).
131+ r == 0 || clamped
132+ }
133+ }
134+
87135#[ cfg( not( any(
88136 target_os = "android" ,
89137 target_vendor = "apple" ,
@@ -125,10 +173,23 @@ impl Condvar {
125173 }
126174}
127175
176+ #[ cfg( target_vendor = "apple" ) ]
177+ impl Condvar {
178+ // `pthread_cond_timedwait_relative_np` measures the timeout
179+ // based on the monotonic clock.
180+ pub const PRECISE_TIMEOUT : bool = true ;
181+
182+ /// # Safety
183+ /// May only be called once per instance of `Self`.
184+ pub unsafe fn init ( self : Pin < & mut Self > ) {
185+ // `PTHREAD_COND_INITIALIZER` is fully supported and we don't need to
186+ // change clocks, so there's nothing to do here.
187+ }
188+ }
189+
128190// `pthread_condattr_setclock` is unfortunately not supported on these platforms.
129191#[ cfg( any(
130192 target_os = "android" ,
131- target_vendor = "apple" ,
132193 target_os = "espidf" ,
133194 target_os = "horizon" ,
134195 target_os = "l4re" ,
0 commit comments