Skip to main content

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}