osal_rs/posix/semaphore.rs
1/***************************************************************************
2 *
3 * osal-rs
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20
21//! Binary and counting semaphores for POSIX.
22//!
23//! See [`Semaphore`] for the concrete type and rationale for why it's built
24//! on a `pthread_mutex_t` + `pthread_cond_t` pair instead of plain POSIX
25//! unnamed semaphores (`sem_t`).
26//!
27//! # Examples
28//!
29//! ```
30//! use osal_rs::os::*;
31//! use osal_rs::utils::OsalRsBool;
32//! use core::time::Duration;
33//!
34//! let sem = Semaphore::new(1, 0).unwrap();
35//! sem.signal();
36//! assert_eq!(sem.wait(Duration::from_millis(100)), OsalRsBool::True);
37//! ```
38
39use core::cell::UnsafeCell;
40use core::ffi::c_long;
41use core::fmt::{Debug, Display};
42use core::ops::Deref;
43use core::time::Duration;
44
45use crate::posix::config::TICK_PERIOD_MS;
46use crate::posix::ffi::{
47 CLOCK_MONOTONIC, ETIMEDOUT, PTHREAD_PRIO_INHERIT, clock_gettime, pthread_cond_broadcast, pthread_cond_destroy, pthread_cond_init, pthread_cond_t, pthread_cond_timedwait, pthread_cond_wait,
48 pthread_condattr_init, pthread_condattr_setclock, pthread_condattr_t, pthread_mutex_destroy, pthread_mutex_init, pthread_mutex_lock, pthread_mutex_t, pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutexattr_init,
49 pthread_mutexattr_setprotocol, pthread_mutexattr_t, timespec,
50};
51use crate::posix::types::{ClockMonotonicHandle, SemaphoreHandle, TickType, UBaseType};
52use crate::traits::{SemaphoreFn, ToTick};
53use crate::utils::{OsalRsBool, Result};
54
55/// Computes an absolute deadline `timeout` from now on the monotonic clock,
56/// for `pthread_cond_timedwait` (this module's condition variables are all
57/// created with `pthread_condattr_setclock(CLOCK_MONOTONIC)`, so its
58/// `abstime` is measured against that same clock).
59fn monotonic_deadline(timeout: Duration) -> timespec {
60 let mut now = timespec::default();
61 unsafe {
62 clock_gettime(CLOCK_MONOTONIC, &mut now);
63 }
64
65 let mut tv_sec = now.tv_sec + timeout.as_secs() as c_long;
66 let mut tv_nsec = now.tv_nsec + timeout.subsec_nanos() as c_long;
67
68 if tv_nsec >= 1_000_000_000 {
69 tv_sec += 1;
70 tv_nsec -= 1_000_000_000;
71 }
72
73 timespec { tv_sec, tv_nsec }
74}
75
76/// POSIX backend for [`SemaphoreFn`]. Built directly on a `pthread_mutex_t` +
77/// `pthread_cond_t` pair rather than `sem_t`, so `signal()` can be bounded by
78/// a user-supplied `max_count` and the mutex can use priority inheritance —
79/// neither of which plain POSIX unnamed semaphores support.
80///
81/// Fields (unnamed, accessed as `self.0`/`self.1`/`self.2`): the pthread
82/// handle, the current count, and the fixed maximum count.
83pub struct Semaphore(UnsafeCell<SemaphoreHandle>, UnsafeCell<UBaseType>, UBaseType);
84
85unsafe impl Send for Semaphore {}
86unsafe impl Sync for Semaphore {}
87
88impl Semaphore {
89 /// Creates a new semaphore with the given maximum and initial count.
90 /// [`Semaphore::signal`] never raises the count above `max_count`; a
91 /// binary semaphore is just `max_count == 1`.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// use osal_rs::os::*;
97 ///
98 /// // Binary semaphore, starts "empty".
99 /// let sem = Semaphore::new(1, 0).unwrap();
100 /// assert_eq!(sem.wait_from_isr(), osal_rs::utils::OsalRsBool::False);
101 /// ```
102 pub fn new(max_count: UBaseType, initial_count: UBaseType) -> Result<Self> {
103
104 let mut mutex: pthread_mutex_t = Default::default();
105 let mut mutex_attr: pthread_mutexattr_t = Default::default();
106 let mut cond: pthread_cond_t = Default::default();
107 let mut cond_attr: pthread_condattr_t = Default::default();
108
109
110 unsafe {
111 // Bind the condvar to CLOCK_MONOTONIC so its absolute timeouts line
112 // up with the clock `monotonic_deadline` uses to build them.
113 pthread_condattr_init(&mut cond_attr);
114 pthread_condattr_setclock (&mut cond_attr, CLOCK_MONOTONIC);
115 pthread_cond_init (&mut cond, &cond_attr);
116 // Priority inheritance: a low-priority holder that blocks a
117 // higher-priority waiter gets temporarily boosted, avoiding
118 // priority inversion (same protocol as posix::mutex::RawMutex).
119 pthread_mutexattr_init (&mut mutex_attr);
120 pthread_mutexattr_setprotocol (&mut mutex_attr, PTHREAD_PRIO_INHERIT);
121 pthread_mutex_init (&mut mutex, &mutex_attr);
122
123 }
124
125 Ok(Self(UnsafeCell::new(ClockMonotonicHandle(mutex, cond)), UnsafeCell::new(initial_count), max_count))
126 }
127
128 // Raw pointers into the `UnsafeCell`s, needed because the pthread FFI
129 // takes `*mut`. `count_ptr()` must only be dereferenced while holding
130 // `mutex_ptr()` locked, except for the racy peek in `is_null()`.
131 fn mutex_ptr(&self) -> *mut pthread_mutex_t {
132 unsafe { &raw mut (*self.0.get()).0 }
133 }
134
135 fn cond_ptr(&self) -> *mut pthread_cond_t {
136 unsafe { &raw mut (*self.0.get()).1 }
137 }
138
139 fn count_ptr(&self) -> *mut UBaseType {
140 self.1.get()
141 }
142
143 // Assumes `mutex_ptr()` is already locked (by the caller, either via
144 // blocking `lock` or non-blocking `trylock`) and always unlocks it
145 // before returning. Increments the count if below `max_count` and, if
146 // so, broadcasts to wake any `wait()`ers.
147 fn signal_locked(&self) -> OsalRsBool {
148 let signalled = unsafe {
149 if *self.count_ptr() < self.2 {
150 *self.count_ptr() += 1;
151 true
152 } else {
153 false
154 }
155 };
156
157 if signalled {
158 // Broadcast, not signal: any thread parked in `wait()`'s loop
159 // could be the one to claim this unit, so all of them are woken
160 // to re-check the count under the mutex; losers just go back to
161 // waiting instead of missing the wake-up.
162 unsafe {
163 pthread_cond_broadcast(self.cond_ptr());
164 }
165 }
166
167 unsafe {
168 pthread_mutex_unlock(self.mutex_ptr());
169 }
170
171 if signalled { OsalRsBool::True } else { OsalRsBool::False }
172 }
173}
174
175impl SemaphoreFn for Semaphore {
176
177 /// Returns `true` if this semaphore is never-initialized-or-already-deleted.
178 ///
179 /// Unlike [`crate::os::EventGroupFn::is_null`], the count is checked too,
180 /// so a live semaphore that just happens to be momentarily empty
181 /// (`count == 0`) is never mistaken for a deleted one.
182 ///
183 /// # Examples
184 ///
185 /// ```
186 /// use osal_rs::os::*;
187 ///
188 /// let mut sem = Semaphore::new(1, 0).unwrap();
189 /// assert!(!sem.is_null());
190 ///
191 /// sem.delete();
192 /// assert!(sem.is_null());
193 /// ```
194 fn is_null(&self) -> bool {
195 unsafe { (*self.0.get()).is_empty() && *self.1.get() == 0 }
196 }
197
198 /// Blocks until a unit is available or `ticks_to_wait` elapses (accepts
199 /// any [`ToTick`] value, e.g. a raw tick count or a [`core::time::Duration`];
200 /// pass [`TickType::MAX`] ticks to wait forever), decrementing the count
201 /// on success.
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// use osal_rs::os::*;
207 /// use osal_rs::utils::OsalRsBool;
208 /// use core::time::Duration;
209 ///
210 /// let sem = Semaphore::new(1, 1).unwrap();
211 /// assert_eq!(sem.wait(Duration::from_millis(100)), OsalRsBool::True);
212 ///
213 /// // Count is now 0: waiting again times out instead of blocking forever.
214 /// assert_eq!(sem.wait(Duration::from_millis(10)), OsalRsBool::False);
215 /// ```
216 fn wait(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
217 if self.is_null() {
218 return OsalRsBool::False
219 }
220
221 let ticks = ticks_to_wait.to_ticks();
222
223 unsafe {
224 pthread_mutex_lock(self.mutex_ptr());
225 }
226
227 // `count > 0` is re-checked in a loop after every wake-up: both
228 // `pthread_cond_wait`/`pthread_cond_timedwait` may return spuriously,
229 // and a woken thread isn't guaranteed to be the one that gets the
230 // unit of the semaphore another thread's `wait()` grabbed first.
231 let acquired = if ticks == TickType::MAX {
232 // TickType::MAX is the "wait forever" sentinel: no deadline,
233 // block until signalled.
234 loop {
235 if unsafe { *self.count_ptr() } > 0 {
236 break true;
237 }
238 unsafe {
239 pthread_cond_wait(self.cond_ptr(), self.mutex_ptr());
240 }
241 }
242 } else {
243 // Bounded wait: the deadline is computed once up front, then
244 // every re-wake races against that same fixed point in time
245 // (rather than restarting a fresh relative timeout each loop).
246 let deadline = monotonic_deadline(Duration::from_millis((ticks as u64).saturating_mul(TICK_PERIOD_MS)));
247
248 loop {
249 if unsafe { *self.count_ptr() } > 0 {
250 break true;
251 }
252 if unsafe { pthread_cond_timedwait(self.cond_ptr(), self.mutex_ptr(), &deadline) } == ETIMEDOUT {
253 break false;
254 }
255 }
256 };
257
258 if acquired {
259 unsafe {
260 *self.count_ptr() -= 1;
261 }
262 }
263
264 unsafe {
265 pthread_mutex_unlock(self.mutex_ptr());
266 }
267
268 if acquired { OsalRsBool::True } else { OsalRsBool::False }
269 }
270
271 /// ISR-safe variant of [`Semaphore::wait`]. POSIX has no interrupt
272 /// context of its own, so this never blocks (`trylock` instead of
273 /// `lock`, and no timeout parameter); it returns [`OsalRsBool::False`]
274 /// both when the mutex is contended and when the count is simply zero.
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use osal_rs::os::*;
280 /// use osal_rs::utils::OsalRsBool;
281 ///
282 /// let sem = Semaphore::new(1, 1).unwrap();
283 /// assert_eq!(sem.wait_from_isr(), OsalRsBool::True);
284 /// assert_eq!(sem.wait_from_isr(), OsalRsBool::False);
285 /// ```
286 fn wait_from_isr(&self) -> OsalRsBool {
287 if self.is_null() {
288 return OsalRsBool::False;
289 }
290
291 // pthreads has no ISR context of its own; `trylock` keeps this
292 // non-blocking, as `_from_isr` callers expect (mirrors
293 // `RawMutex::lock_from_isr`). If the mutex is contended, bail out
294 // rather than blocking the "interrupt".
295 if unsafe { pthread_mutex_trylock(self.mutex_ptr()) } != 0 {
296 return OsalRsBool::False;
297 }
298
299 let acquired = unsafe {
300 if *self.count_ptr() > 0 {
301 *self.count_ptr() -= 1;
302 true
303 } else {
304 false
305 }
306 };
307
308 unsafe {
309 pthread_mutex_unlock(self.mutex_ptr());
310 }
311
312 if acquired { OsalRsBool::True } else { OsalRsBool::False }
313 }
314
315 /// Increments the count (unless already at `max_count`) and wakes any
316 /// thread blocked in [`Semaphore::wait`]. Returns [`OsalRsBool::False`]
317 /// if the semaphore was already at `max_count`.
318 ///
319 /// # Examples
320 ///
321 /// ```
322 /// use osal_rs::os::*;
323 /// use osal_rs::utils::OsalRsBool;
324 ///
325 /// let sem = Semaphore::new(1, 0).unwrap();
326 /// assert_eq!(sem.signal(), OsalRsBool::True);
327 ///
328 /// // Already at max_count: signalling again fails.
329 /// assert_eq!(sem.signal(), OsalRsBool::False);
330 /// ```
331 fn signal(&self) -> OsalRsBool {
332 if self.is_null() {
333 return OsalRsBool::False;
334 }
335
336 unsafe {
337 pthread_mutex_lock(self.mutex_ptr());
338 }
339
340 self.signal_locked()
341 }
342
343 /// ISR-safe variant of [`Semaphore::signal`]. Fails with
344 /// [`OsalRsBool::False`] instead of blocking if the mutex is contended.
345 ///
346 /// # Examples
347 ///
348 /// ```
349 /// use osal_rs::os::*;
350 /// use osal_rs::utils::OsalRsBool;
351 ///
352 /// let sem = Semaphore::new(1, 0).unwrap();
353 /// assert_eq!(sem.signal_from_isr(), OsalRsBool::True);
354 /// ```
355 fn signal_from_isr(&self) -> OsalRsBool {
356 if self.is_null() {
357 return OsalRsBool::False;
358 }
359
360 // Same non-blocking rationale as `wait_from_isr`: `trylock` instead
361 // of `lock` so this never blocks the "interrupt".
362 if unsafe { pthread_mutex_trylock(self.mutex_ptr()) } != 0 {
363 return OsalRsBool::False;
364 }
365
366 self.signal_locked()
367 }
368
369 /// Destroys the underlying pthread objects and resets this semaphore to
370 /// its "null" state. Safe to call more than once, and called
371 /// automatically on [`Drop`] if not called explicitly.
372 ///
373 /// # Examples
374 ///
375 /// ```
376 /// use osal_rs::os::*;
377 ///
378 /// let mut sem = Semaphore::new(1, 0).unwrap();
379 /// sem.delete();
380 /// assert!(sem.is_null());
381 /// ```
382 fn delete(&mut self) {
383 if self.is_null() {
384 return;
385 }
386
387 unsafe {
388 pthread_mutex_destroy(self.mutex_ptr());
389 pthread_cond_destroy(self.cond_ptr());
390 }
391
392 // Reset to the "null" state so a second `delete()` call (e.g. from
393 // `Drop` after an explicit `delete()`) is a no-op rather than
394 // destroying the same pthread objects twice.
395 *self.0.get_mut() = SemaphoreHandle::default();
396 *self.1.get_mut() = 0;
397 }
398}
399
400impl Drop for Semaphore {
401 fn drop(&mut self) {
402 if self.is_null() {
403 return;
404 }
405 // Safety net for callers that don't call `delete()` explicitly.
406 self.delete();
407 }
408}
409
410impl Deref for Semaphore {
411 type Target = SemaphoreHandle;
412
413 fn deref(&self) -> &Self::Target {
414 // Read-only escape hatch to the raw (mutex, condvar) handle.
415 unsafe { &*self.0.get() }
416 }
417}
418
419impl Debug for Semaphore {
420 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
421 f.debug_struct("Semaphore")
422 .field("handle", unsafe { &*self.0.get() })
423 .field("count", unsafe { &*self.1.get() })
424 .field("max_count", &self.2)
425 .finish()
426 }
427}
428
429impl Display for Semaphore {
430 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
431 write!(f, "Semaphore {{ handle: {:?}, count: {}, max_count: {} }}", unsafe { &*self.0.get() }, unsafe { *self.1.get() }, self.2)
432 }
433}