Merge pull request #5037 from Feuris/feat/libc-cnd-timedwait

feat(libc): implement _Cnd_timedwait
This commit is contained in:
Adrià 2026-10-13 12:26:09 +02:00 committed by GitHub
commit c7632f036f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 167 additions and 4 deletions

View File

@ -639,6 +639,18 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp)
target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc)
configure_windows_unwind(guest_libc_cnd_timed_wait_tests)
if(BUILD_TESTING)
set_target_properties(guest_libc_cnd_timed_wait_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME guest_libc_cnd_timed_wait COMMAND guest_libc_cnd_timed_wait_tests)
if(WIN32)
set_tests_properties(guest_libc_cnd_timed_wait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(MINGW)
if(BUILD_TESTING)
enable_language(ASM)

View File

@ -3,6 +3,7 @@
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <limits>
@ -24,18 +25,18 @@ struct Xtime {
constexpr std::int64_t nanosPerSecond = 1000000000;
constexpr std::int64_t longestSleepSeconds = 86400;
timespec XtimeDeadline(const Xtime& time) {
timespec XtimeDeadline(const Xtime& time, const char* function) {
constexpr auto largest = std::numeric_limits<std::int64_t>::max();
constexpr auto smallest = std::numeric_limits<std::int64_t>::min();
const auto carry = time.nsec / nanosPerSecond;
auto nanos = time.nsec % nanosPerSecond;
auto seconds = time.sec;
if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry))
throw std::overflow_error("_Thrd_sleep: xtime seconds overflow");
throw std::overflow_error(std::string(function) + ": xtime seconds overflow");
seconds += carry;
if (nanos < 0) {
if (seconds == smallest)
throw std::overflow_error("_Thrd_sleep: xtime seconds overflow");
throw std::overflow_error(std::string(function) + ": xtime seconds overflow");
nanos += nanosPerSecond;
--seconds;
}
@ -44,6 +45,7 @@ timespec XtimeDeadline(const Xtime& time) {
constexpr int threadSuccess = 0;
constexpr int threadNomem = 1;
constexpr int threadTimedout = 2;
constexpr int threadBusy = 3;
constexpr int threadError = 4;
@ -220,6 +222,44 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle)
return threadSuccess;
}
int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) {
if (!target)
APS5_INVALID_ARG_EX;
const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait");
constexpr auto maxDuration = std::chrono::system_clock::duration::max();
constexpr auto minDuration = std::chrono::system_clock::duration::min();
constexpr auto maxSeconds = std::chrono::duration_cast<std::chrono::seconds>(maxDuration).count();
constexpr auto minSeconds = std::chrono::duration_cast<std::chrono::seconds>(minDuration).count();
constexpr auto maxNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(maxDuration - std::chrono::seconds(maxSeconds)).count();
if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) ||
deadline.tv_sec < minSeconds)
throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow");
auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait");
auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait");
if (!IsOwnedByCaller(mutex))
return threadSuccess;
if (mutex->count != 1)
return threadSuccess;
mutex->count = 0;
mutex->owner.store(std::thread::id{}, std::memory_order_release);
std::unique_lock lock(mutex->native, std::adopt_lock);
bool timeout = false;
try {
const auto deadlineTime = std::chrono::system_clock::time_point(
std::chrono::seconds(deadline.tv_sec) + std::chrono::nanoseconds(deadline.tv_nsec));
timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout);
} catch (const std::system_error&) {
lock.release();
mutex->owner.store(std::this_thread::get_id(), std::memory_order_release);
mutex->count = 1;
throw;
}
lock.release();
mutex->owner.store(std::this_thread::get_id(), std::memory_order_release);
mutex->count = 1;
return timeout ? threadTimedout : threadSuccess;
}
int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) {
ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all();
return threadSuccess;
@ -233,7 +273,7 @@ int SignalCondition_nid_no_patch(void** handle) {
int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) {
if (!target)
APS5_INVALID_ARG_EX;
const auto deadline = XtimeDeadline(*target);
const auto deadline = XtimeDeadline(*target, "_Thrd_sleep");
for (;;) {
timespec now{};
if (clock_gettime(CLOCK_REALTIME, &now) != 0)

View File

@ -0,0 +1,110 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <stdexcept>
#include <thread>
struct Xtime {
std::int64_t sec;
std::int64_t nsec;
};
extern "C" {
int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type);
void APS5_VABI _Mtx_destroy_nid_postfix(void** handle);
int APS5_VABI _Mtx_lock_nid_postfix(void** handle);
int APS5_VABI _Mtx_unlock_nid_postfix(void** handle);
int APS5_VABI _Cnd_init_nid_postfix(void** handle);
void APS5_VABI _Cnd_destroy_nid_postfix(void** handle);
int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle);
int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target);
}
static void Require(bool value) { if (!value) std::abort(); }
template<typename TException, typename TCall>
static void RequireThrows(TCall call) {
try {
call();
} catch (const TException&) {
return;
}
std::abort();
}
constexpr int success = 0;
constexpr int timedout = 2;
constexpr int plain = 0x01;
int main() {
void* mutex = nullptr;
void* condition = nullptr;
Require(_Mtx_init_nid_postfix(&mutex, plain) == success);
Require(_Cnd_init_nid_postfix(&condition) == success);
Require(_Mtx_lock_nid_postfix(&mutex) == success);
const auto now = std::chrono::system_clock::now();
const auto nowSec = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
const auto nowNsec = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count() % 1000000000;
Xtime pastTime{nowSec - 1, nowNsec};
Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &pastTime) == timedout);
Xtime unnormalizedPastTime{nowSec - 2, nowNsec + 1000000000};
Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedPastTime) == timedout);
Xtime negativePastTime{nowSec, nowNsec - 2000000000};
Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &negativePastTime) == timedout);
const auto timeoutDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20);
const auto timeoutSec = std::chrono::duration_cast<std::chrono::seconds>(timeoutDeadline.time_since_epoch()).count();
const auto timeoutNsec = std::chrono::duration_cast<std::chrono::nanoseconds>(timeoutDeadline.time_since_epoch()).count() % 1000000000;
const Xtime timeoutTime{timeoutSec, timeoutNsec};
while (true) {
const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &timeoutTime);
if (result == timedout) {
Require(std::chrono::system_clock::now() >= timeoutDeadline);
break;
}
Require(result == success);
}
const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20);
const auto unnormalizedSec = std::chrono::duration_cast<std::chrono::seconds>(unnormalizedDeadline.time_since_epoch()).count();
const auto unnormalizedNsec = std::chrono::duration_cast<std::chrono::nanoseconds>(unnormalizedDeadline.time_since_epoch()).count() % 1000000000;
const Xtime unnormalizedTimeoutTime{unnormalizedSec - 1, unnormalizedNsec + 1000000000};
while (true) {
const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedTimeoutTime);
if (result == timedout) {
Require(std::chrono::system_clock::now() >= unnormalizedDeadline);
break;
}
Require(result == success);
}
Xtime futureTime{nowSec + 5, nowNsec};
bool ready = false;
std::thread signaler([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Require(_Mtx_lock_nid_postfix(&mutex) == success);
ready = true;
Require(_Cnd_broadcast_nid_postfix(&condition) == success);
Require(_Mtx_unlock_nid_postfix(&mutex) == success);
});
while (!ready) {
Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &futureTime) == success);
}
signaler.join();
Require(_Mtx_unlock_nid_postfix(&mutex) == success);
RequireThrows<std::invalid_argument>([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, nullptr); });
const Xtime overflowCarryTime{std::numeric_limits<std::int64_t>::max(), 1000000000};
RequireThrows<std::overflow_error>([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowCarryTime); });
const Xtime overflowSecTime{std::numeric_limits<std::int64_t>::max(), 0};
RequireThrows<std::overflow_error>([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowSecTime); });
_Cnd_destroy_nid_postfix(&condition);
_Mtx_destroy_nid_postfix(&mutex);
}

View File

@ -6,6 +6,7 @@
- [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified.
- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched.
- [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter.
- [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws.
- [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws
- [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error
- [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error