summaryrefslogtreecommitdiff
path: root/src/pthread.rs
blob: b549e1d429dff703fc439c4ad6ee87c63ae606d3 (plain)
use core::cell::UnsafeCell;
use core::ops::Deref;

use alloc::boxed::Box;

use libc::{
	pthread_mutex_lock, pthread_mutex_t, pthread_mutex_trylock, pthread_mutex_unlock, EBUSY,
	PTHREAD_MUTEX_INITIALIZER,
};

use crate::lazy_box::{LazyBox, LazyInit};

struct RawMutex(UnsafeCell<pthread_mutex_t>);

impl LazyInit for RawMutex {
	fn init() -> Box<Self> {
		// We need to box this no matter what, because copying a pthread mutex
		// results in undocumented behavior.
		let mutex = Box::new(Self(UnsafeCell::new(PTHREAD_MUTEX_INITIALIZER)));

		if !cfg!(feature = "unsafe_lock") {
			// A pthread mutex initialized with PTHREAD_MUTEX_INITIALIZER will have
			// a type of PTHREAD_MUTEX_DEFAULT, which has undefined behavior if you
			// try to re-lock it from the same thread when you already hold a lock
			// (https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutex_init.html).
			// This is the case even if PTHREAD_MUTEX_DEFAULT == PTHREAD_MUTEX_NORMAL
			// (https://github.com/rust-lang/rust/issues/33770#issuecomment-220847521) -- in that
			// case, `pthread_mutexattr_settype(PTHREAD_MUTEX_DEFAULT)` will of course be the same
			// as setting it to `PTHREAD_MUTEX_NORMAL`, but not setting any mode will result in
			// a Mutex where re-locking is UB.
			//
			// In practice, glibc takes advantage of this undefined behavior to
			// implement hardware lock elision, which uses hardware transactional
			// memory to avoid acquiring the lock. While a transaction is in
			// progress, the lock appears to be unlocked. This isn't a problem for
			// other threads since the transactional memory will abort if a conflict
			// is detected, however no abort is generated when re-locking from the
			// same thread.
			//
			// Since locking the same mutex twice will result in two aliasing &mut
			// references, we instead create the mutex with type
			// PTHREAD_MUTEX_NORMAL which is guaranteed to deadlock if we try to
			// re-lock it from the same thread, thus avoiding undefined behavior.
			unsafe {
				let mut attr = MaybeUninit::<libc::pthread_mutexattr_t>::uninit();
				libc::pthread_mutexattr_init(attr.as_mut_ptr());
				let attr = PthreadMutexAttr(&mut attr);
				libc::pthread_mutexattr_settype(attr.0.as_mut_ptr(), libc::PTHREAD_MUTEX_NORMAL);
				libc::pthread_mutex_init(mutex.0.get(), attr.0.as_ptr());
			}
		}

		mutex
	}
}

impl Deref for RawMutex {
	type Target = UnsafeCell<pthread_mutex_t>;

	fn deref(&self) -> &Self::Target {
		&self.0
	}
}

pub struct Mutex(LazyBox<RawMutex>);

unsafe impl Send for Mutex {}
unsafe impl Sync for Mutex {}

impl Mutex {
	#[inline]
	pub const fn new() -> Self {
		Self(LazyBox::new())
	}

	/// Locks the mutex
	///
	/// # Safety
	///
	/// UB occurs if the mutex is already locked by the current thread and the
	/// `unsafe_lock` feature is enabled.
	#[inline]
	pub unsafe fn lock(&self) {
		// safety: the pointer is valid
		pthread_mutex_lock(self.0.get());
	}

	/// If the mutex is unlocked, it is locked, and this function returns
	/// `true'. Otherwise, `false` is returned.
	#[inline]
	pub unsafe fn try_lock(&self) -> bool {
		// safety: the pointer is valid
		unsafe { pthread_mutex_trylock(self.0.get()) == 0 }
	}

	/// Unlocks the mutex
	///
	/// # Safety
	///
	/// UB occurs if the mutex is already unlocked or if it has been locked on
	/// a different thread.
	#[inline]
	pub unsafe fn unlock(&self) {
		// safety: the pointer is valid
		pthread_mutex_unlock(self.0.get());
	}

	pub unsafe fn is_locked(&self) -> bool {
		if self.try_lock() {
			unsafe {
				self.unlock();
			}
			false
		} else {
			true
		}
	}
}