mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-27 18:00:48 +01:00
- Do compiler/architecture checks EARLY, don't waste time doing random configuration stuff if the user can't even compile in the first place - MSVC is unsupported, I assume? So add a check to disallow. There's literally no MSVC or MSC_VER checks anywhere, so... - Rather than using the MSVC architecture definitions, use our own `ARCHITECTURE_arm64` et al. Hijacking existing "standard" definitions is a very bad idea. Also makes it more readable in CMake - Change the x86 host check to `x86|amd64`. Some systems still refer to themselves as x86 despite being 64-bit for... reasons, and I saw one a very long time ago that referred to it as amd64. This should basically never come up, nor is it really relevant given that FEX is for arm64... but it kinda annoyed me so whatever. TODOs: - Should we check `CMAKE_SIZEOF_VOID_P (equal) 64`? I don't think anyone is even trying to compile this thing on armv7 or older, but might as well? maybe? - What's the status of *BSD, Solaris, macOS? Technically macOS does support Wine, not sure about the others. Signed-off-by: crueter <crueter@eden-emu.dev>
268 lines
9.5 KiB
C++
268 lines
9.5 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#pragma once
|
|
|
|
#include <FEXCore/Debug/InternalThreadState.h>
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <signal.h>
|
|
#ifndef _WIN32
|
|
#include <sys/syscall.h>
|
|
#endif
|
|
#include <unistd.h>
|
|
#include <variant>
|
|
|
|
namespace FEXCore {
|
|
#ifndef _WIN32
|
|
// Replacement for std::mutexes to deal with unlocking issues in the face of Linux fork() semantics.
|
|
//
|
|
// A fork() only clones the parent's calling thread. Other threads are silently dropped, which permanently leaves any mutexes owned by them locked.
|
|
// To address this issue, ForkableUniqueMutex and ForkableSharedMutex provide a way to forcefully remove any dangling locks and reset the mutexes to their default state.
|
|
class ForkableUniqueMutex final {
|
|
public:
|
|
ForkableUniqueMutex()
|
|
: Mutex(PTHREAD_MUTEX_INITIALIZER) {}
|
|
|
|
// Move-only type
|
|
ForkableUniqueMutex(const ForkableUniqueMutex&) = delete;
|
|
ForkableUniqueMutex& operator=(const ForkableUniqueMutex&) = delete;
|
|
ForkableUniqueMutex(ForkableUniqueMutex&& rhs) = default;
|
|
ForkableUniqueMutex& operator=(ForkableUniqueMutex&&) = default;
|
|
|
|
void lock() {
|
|
const auto Result = pthread_mutex_lock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result);
|
|
}
|
|
void unlock() {
|
|
const auto Result = pthread_mutex_unlock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == 0, "{} failed to unlock with {}", __func__, Result);
|
|
}
|
|
// Initialize the internal pthread object to its default initializer state.
|
|
// Should only ever be used in the child process when a Linux fork() has occured.
|
|
void StealAndDropActiveLocks() {
|
|
Mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
}
|
|
|
|
// Asserts that the mutex isn't exclusively owned by the calling thread.
|
|
void check_lock_owned_by_self() {
|
|
const auto Result = pthread_mutex_lock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == EDEADLK, "User of unique lock must have already locked mutex as write!");
|
|
}
|
|
|
|
private:
|
|
pthread_mutex_t Mutex;
|
|
};
|
|
|
|
class ForkableSharedMutex final {
|
|
public:
|
|
ForkableSharedMutex()
|
|
: Mutex(PTHREAD_RWLOCK_INITIALIZER) {}
|
|
|
|
// Move-only type
|
|
ForkableSharedMutex(const ForkableSharedMutex&) = delete;
|
|
ForkableSharedMutex& operator=(const ForkableSharedMutex&) = delete;
|
|
ForkableSharedMutex(ForkableSharedMutex&& rhs) = default;
|
|
ForkableSharedMutex& operator=(ForkableSharedMutex&&) = default;
|
|
|
|
void lock() {
|
|
const auto Result = pthread_rwlock_wrlock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result);
|
|
}
|
|
void unlock() {
|
|
const auto Result = pthread_rwlock_unlock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == 0, "{} failed to unlock with {}", __func__, Result);
|
|
}
|
|
void lock_shared() {
|
|
const auto Result = pthread_rwlock_rdlock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result);
|
|
}
|
|
|
|
void unlock_shared() {
|
|
unlock();
|
|
}
|
|
|
|
bool try_lock() {
|
|
const auto Result = pthread_rwlock_trywrlock(&Mutex);
|
|
return Result == 0;
|
|
}
|
|
|
|
bool try_lock_shared() {
|
|
const auto Result = pthread_rwlock_tryrdlock(&Mutex);
|
|
return Result == 0;
|
|
}
|
|
|
|
// Asserts that the rwlock isn't exclusively owned by the calling thread.
|
|
void check_lock_owned_by_self_as_write() {
|
|
const auto Result = pthread_rwlock_wrlock(&Mutex);
|
|
LOGMAN_THROW_A_FMT(Result == EDEADLK, "User of rwlock must have already locked mutex as write!");
|
|
}
|
|
|
|
// Initialize the internal pthread object to its default initializer state.
|
|
// Should only ever be used in the child process when a Linux fork() has occured.
|
|
void StealAndDropActiveLocks() {
|
|
Mutex = PTHREAD_RWLOCK_INITIALIZER;
|
|
}
|
|
private:
|
|
pthread_rwlock_t Mutex;
|
|
};
|
|
|
|
// Helper class to manage deferred signal refcounting within a block scope
|
|
class DeferredSignalRefCountGuard final {
|
|
public:
|
|
explicit DeferredSignalRefCountGuard(FEXCore::Core::InternalThreadState* Thread)
|
|
: Thread(Thread) {
|
|
// Needs to be atomic so that operations can't end up getting reordered around this.
|
|
Thread->CurrentFrame->State.DeferredSignalRefCount.Increment(1);
|
|
}
|
|
|
|
// Move-only type
|
|
DeferredSignalRefCountGuard(const DeferredSignalRefCountGuard&) = delete;
|
|
DeferredSignalRefCountGuard& operator=(DeferredSignalRefCountGuard&) = delete;
|
|
DeferredSignalRefCountGuard(DeferredSignalRefCountGuard&& rhs)
|
|
: Thread(rhs.Thread) {
|
|
rhs.Thread = nullptr;
|
|
}
|
|
|
|
~DeferredSignalRefCountGuard() {
|
|
if (Thread) {
|
|
#ifdef ARCHITECTURE_x86_64
|
|
// Needs to be atomic so that operations can't end up getting reordered around this.
|
|
// Without this, the refcount and the signal access could get reordered.
|
|
auto Result = Thread->CurrentFrame->State.DeferredSignalRefCount.Decrement(1);
|
|
|
|
// X86-64 must do an additional check around the store.
|
|
if ((Result - 1) == 0) {
|
|
// Must happen after the refcount store
|
|
auto InterruptFaultPage = reinterpret_cast<Core::NonAtomicRefCounter<uint64_t>*>(&Thread->InterruptFaultPage);
|
|
InterruptFaultPage->Store(0);
|
|
}
|
|
#else
|
|
Thread->CurrentFrame->State.DeferredSignalRefCount.Decrement(1);
|
|
auto InterruptFaultPage = reinterpret_cast<Core::NonAtomicRefCounter<uint64_t>*>(&Thread->InterruptFaultPage);
|
|
InterruptFaultPage->Store(0);
|
|
#endif
|
|
}
|
|
}
|
|
private:
|
|
FEXCore::Core::InternalThreadState* Thread;
|
|
};
|
|
|
|
// Helper class to mask POSIX signals within a block scope
|
|
class ScopedSignalMasker final {
|
|
public:
|
|
explicit ScopedSignalMasker(uint64_t Mask)
|
|
: OriginalMask(0) {
|
|
// Mask all signals, storing the original incoming mask
|
|
::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &Mask, &*OriginalMask, sizeof(*OriginalMask));
|
|
}
|
|
|
|
// Move-only type
|
|
ScopedSignalMasker(const ScopedSignalMasker&) = delete;
|
|
ScopedSignalMasker& operator=(ScopedSignalMasker&) = delete;
|
|
ScopedSignalMasker(ScopedSignalMasker&& rhs)
|
|
: OriginalMask(rhs.OriginalMask) {
|
|
rhs.OriginalMask.reset();
|
|
}
|
|
|
|
~ScopedSignalMasker() {
|
|
if (OriginalMask) {
|
|
::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &OriginalMask, nullptr, sizeof(*OriginalMask));
|
|
}
|
|
}
|
|
private:
|
|
std::optional<uint64_t> OriginalMask {};
|
|
};
|
|
|
|
/**
|
|
* @brief Produces a wrapper object around a scoped lock of the given mutex
|
|
* while ensuring POSIX signals are masked while the mutex is locked
|
|
*
|
|
* Use this to prevent reentrancy issues of C++ mutexes with certain signal handlers.
|
|
* Common examples of such issues are:
|
|
* - C++ mutexes not unlocking due to a signal handler calling longjmp from within a scope owning the mutex
|
|
* - The signal handler itself using a mutex that would be re-locked if the handler gets invoked
|
|
* again before unlocking
|
|
*
|
|
* Ownership of the returned object may be moved, but it is NOT SAFE to move across threads.
|
|
*/
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto MaskSignalsAndLockMutex(MutexType& mutex, uint64_t Mask = ~0ULL) {
|
|
// Signals are masked first, and then the lock is acquired
|
|
struct {
|
|
ScopedSignalMasker mask;
|
|
LockType<MutexType> lock;
|
|
} scope_guard {ScopedSignalMasker {Mask}, LockType<MutexType> {mutex}};
|
|
return scope_guard;
|
|
}
|
|
|
|
/**
|
|
* @brief Produces a wrapper object around a scoped lock of the given mutex
|
|
* while bumping the Thread's deferred signal refcount while the mutex is
|
|
* locked.
|
|
*/
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto GuardSignalDeferringSection(MutexType& mutex, FEXCore::Core::InternalThreadState* Thread, uint64_t Mask = ~0ULL) {
|
|
// Refcount is incremented first, and then the lock is acquired.
|
|
struct {
|
|
std::optional<DeferredSignalRefCountGuard> refcount;
|
|
LockType<MutexType> lock;
|
|
} scope_guard = {DeferredSignalRefCountGuard {Thread}, LockType<MutexType> {mutex}};
|
|
return scope_guard;
|
|
}
|
|
|
|
// Like GuardSignalDeferringSection but falls back to masking signals when Thread is nullptr
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto GuardSignalDeferringSectionWithFallback(MutexType& mutex, FEXCore::Core::InternalThreadState* Thread, uint64_t Mask = ~0ULL) {
|
|
using ExtraGuard = std::variant<ScopedSignalMasker, DeferredSignalRefCountGuard>;
|
|
|
|
struct {
|
|
ExtraGuard refcount_or_mask;
|
|
LockType<MutexType> lock;
|
|
} scope_guard {Thread ? ExtraGuard {DeferredSignalRefCountGuard {Thread}} : ExtraGuard {ScopedSignalMasker {Mask}}};
|
|
scope_guard.lock = LockType<MutexType> {mutex};
|
|
return scope_guard;
|
|
}
|
|
|
|
#else
|
|
|
|
// Dummy implementations as Windows doesn't support forking or async signals.
|
|
class ForkableUniqueMutex final : public std::mutex {
|
|
public:
|
|
void StealAndDropActiveLocks() {
|
|
LogMan::Msg::AFmt("{} is unsupported on WIN32 builds!", __func__);
|
|
}
|
|
};
|
|
|
|
class ForkableSharedMutex final : public std::shared_mutex {
|
|
public:
|
|
void StealAndDropActiveLocks() {
|
|
LogMan::Msg::AFmt("{} is unsupported on WIN32 builds!", __func__);
|
|
}
|
|
};
|
|
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto MaskSignalsAndLockMutex(MutexType& mutex, uint64_t Mask = ~0ULL) {
|
|
return LockType<MutexType> {mutex};
|
|
}
|
|
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto GuardSignalDeferringSection(MutexType& mutex, FEXCore::Core::InternalThreadState* Thread, uint64_t Mask = ~0ULL) {
|
|
return LockType<MutexType> {mutex};
|
|
}
|
|
|
|
template<template<typename> class LockType = std::unique_lock, typename MutexType>
|
|
[[nodiscard]]
|
|
static auto GuardSignalDeferringSectionWithFallback(MutexType& mutex, FEXCore::Core::InternalThreadState* Thread, uint64_t Mask = ~0ULL) {
|
|
return LockType<MutexType> {mutex};
|
|
}
|
|
|
|
#endif
|
|
} // namespace FEXCore
|