diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt new file mode 100644 index 000000000..3aae2c755 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) +configure_windows_unwind(libSceLibcInternal) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_thread_atexit_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadAtexit.cpp) + target_include_directories(guest_thread_atexit_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_thread_atexit_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_thread_atexit_tests) + add_test(NAME guest_thread_atexit COMMAND guest_thread_atexit_tests) + set_tests_properties(guest_thread_atexit PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 49699b6ca..c84ee09a1 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -1,5 +1,69 @@ +#include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/HeapDiagnostics.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" void APS5_VABI sceKernelSetThreadDtors(thread_dtors_func_t dtors); + +namespace { + +using ThreadDestructorFunction = void (APS5_VABI*)(void*); + +struct ThreadDestructor { + ThreadDestructorFunction function; + void* object; + void* dsoSymbol; +}; + +struct ThreadDestructorsTag {}; + +std::vector& ThreadDestructors() { + return HostThreadLocal, ThreadDestructorsTag>(); +} + +bool IsInLoadedImage(const void* address) { + if (address == nullptr) + return false; +#ifdef _WIN32 + HMODULE module = nullptr; + return GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, static_cast(address), &module) != 0; +#else + Dl_info info{}; + return dladdr(address, &info) != 0; +#endif +} + +void APS5_VABI RunThreadDestructors_nid_no_patch() { + auto& destructors = ThreadDestructors(); + while (!destructors.empty()) { + const ThreadDestructor destructor = destructors.back(); + destructors.pop_back(); + const auto* function = reinterpret_cast(destructor.function); + if (!IsInLoadedImage(function)) { + std::ostringstream message; + message << "thread_local destructor " << function << " of dso " << destructor.dsoSymbol << " is not in a loaded image"; + throw std::runtime_error(message.str()); + } + destructor.function(destructor.object); + } +} + +void RegisterThreadExitHook() { + [[maybe_unused]] static const bool registered = [] { + sceKernelSetThreadDtors(RunThreadDestructors_nid_no_patch); + return true; + }(); +} + +} extern "C" { @@ -13,4 +77,20 @@ void APS5_VABI sceLibcHeapGetTraceInfo_nid_postfix(Info* info) { LibcHeapTraceInfo_nid_no_patch(info); } +int APS5_VABI _sceLibcInternalThreadAtexit_nid_postfix(ThreadDestructorFunction destructor, void* object, void* dsoSymbol) { + RegisterThreadExitHook(); + ThreadDestructors().push_back({destructor, object, dsoSymbol}); + return 0; +} + +void APS5_VABI _sceLibcInternalThreadDtors_nid_postfix() { + RunThreadDestructors_nid_no_patch(); +} + +int APS5_VABI _sceLibcInternalForceTlsDestructor_nid_postfix(KernelModule handle) { + (void)handle; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestThreadAtexit.cpp b/core/libs/tests/GuestThreadAtexit.cpp new file mode 100644 index 000000000..f37fd5d52 --- /dev/null +++ b/core/libs/tests/GuestThreadAtexit.cpp @@ -0,0 +1,87 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI _sceLibcInternalThreadAtexit_nid_postfix(void (APS5_VABI* destructor)(void*), void* object, void* dsoSymbol); +void APS5_VABI _sceLibcInternalThreadDtors_nid_postfix(); +} + +namespace { + +using Destructor = void (APS5_VABI*)(void*); + +void Require(bool value) { if (!value) std::abort(); } + +int dsoHandle = 0; +std::vector calls; +std::vector callers; + +void* Object(std::intptr_t value) { return reinterpret_cast(value); } + +int Register(Destructor destructor, std::intptr_t object) { + return _sceLibcInternalThreadAtexit_nid_postfix(destructor, Object(object), &dsoHandle); +} + +void APS5_VABI Record(void* object) { + calls.push_back(reinterpret_cast(object)); + callers.push_back(scePthreadSelf()); +} + +void APS5_VABI RecordAndRegister(void* object) { + Record(object); + Require(Register(Record, 9) == 0); +} + +void* APS5_VABI Worker(void*) { + Require(Register(Record, 4) == 0); + Require(Register(Record, 5) == 0); + return nullptr; +} + +bool DestructorsThrow() { + try { + _sceLibcInternalThreadDtors_nid_postfix(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + const Pthread mainThread = scePthreadSelf(); + Require(Register(Record, 1) == 0); + Require(Register(RecordAndRegister, 2) == 0); + Require(Register(Record, 3) == 0); + _sceLibcInternalThreadDtors_nid_postfix(); + Require(calls == std::vector{3, 2, 9, 1}); + Require(callers == std::vector(4, mainThread)); + _sceLibcInternalThreadDtors_nid_postfix(); + Require(calls.size() == 4); + + calls.clear(); + callers.clear(); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, Worker, nullptr, "ThreadAtexit") == 0); + Require(scePthreadJoin(worker, nullptr) == 0); + Require(calls == std::vector{5, 4}); + Require(callers == std::vector(2, worker)); + + calls.clear(); + std::vector heap(64); + Require(Register(Record, 6) == 0); + Require(Register(nullptr, 7) == 0); + Require(Register(reinterpret_cast(heap.data()), 8) == 0); + Require(DestructorsThrow()); + Require(DestructorsThrow()); + Require(calls.empty()); + _sceLibcInternalThreadDtors_nid_postfix(); + Require(calls == std::vector{6}); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a84ead929..a490dce87 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,6 +44,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw +- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature @@ -101,3 +103,4 @@ Throughout the project, every function at every stage either **does exactly what - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. +- [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped