Wii U: newlib locks on Cafe OS mutexes

devkitPPC's newlib serialises stdio, the environment, the time zone
and the atexit lists only through the weak __syscall_lock_* hooks,
which nothing on Wii U defined, so with threads on, libc state shared
between the frontend's threads went unlocked. wiiu/system/newlib_locks.c
provides them on OSMutex: newlib's lock word starts at 0, a mutex is
created on first use and published into it by compare-and-swap, and
OSMutex being recursive serves the recursive locks as well.

samples/frontend/wiiu_newlib_locks runs the hooks against stand-in
mutexes: threads racing first use leave one mutex published and the
counter it guards exact, plus the recursive, try and close paths.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-26 03:46:36 +01:00
1 parent 412d2c4224
commit 06a85bc900
5 files changed
+368

No files matched your search

+1
View File
@@ -43,6 +43,7 @@ OBJ += wiiu/system/memory.o
OBJ += wiiu/system/atomic.o
OBJ += wiiu/system/exception_handler.o
OBJ += wiiu/system/missing_libc_functions.o
OBJ += wiiu/system/newlib_locks.o
OBJ += wiiu/fs/sd_fat_devoptab.o
OBJ += wiiu/fs/fs_utils.o
OBJ += wiiu/hbl.o
@@ -0,0 +1,37 @@
# wiiu/system/newlib_locks.c, the newlib lock hooks on Cafe OS mutexes,
# against stand-in OSMutex functions. The Wii U headers are the tree's
# own (wiiu/include). Builds for Linux and, with
# CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine, for Windows.
TARGET := wiiu_newlib_locks_test$(EXE)
CORE_DIR := ../../..
CFLAGS += -std=gnu89 -Wall -g1 -O1 -I$(CORE_DIR)/wiiu/include
LDFLAGS += -lpthread
# The lock word is an int holding a mutex address, as on the Wii U, so
# the test's static pool must sit below 2^31.
ifeq ($(EXE),.exe)
LDFLAGS += -static -Wl,--image-base=0x10000000 -Wl,--disable-dynamicbase \
-Wl,--disable-high-entropy-va
else
CFLAGS += -fno-pie
LDFLAGS += -no-pie
endif
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
$(TARGET): wiiu_newlib_locks_test.c $(CORE_DIR)/wiiu/system/newlib_locks.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
check: all
$(RUNNER) ./$(TARGET)
clean:
rm -f wiiu_newlib_locks_test wiiu_newlib_locks_test.exe
.PHONY: all check clean
@@ -0,0 +1,194 @@
/* wiiu/system/newlib_locks.c against stand-in Cafe OS mutexes: newlib's
* lock word starts at 0, several threads race to create its mutex,
* exactly one mutex is published and kept, and it excludes. The
* recursive, try and close paths are checked on one thread and
* across two.
*
* newlib's lock word is an int holding the mutex address, as on the
* Wii U's 32-bit address space, so the stand-in allocator hands out
* slots from a static pool and the executable is linked at a low
* fixed base (see the Makefile) to keep those addresses below 2^31. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
#include <wiiu/os/mutex.h>
#define POOL_SLOTS 64
#define THREADS 8
#define ROUNDS 20000
struct slot
{
OSMutex os; /* first: the pointer the code under test sees */
pthread_mutex_t pm;
int live;
};
static struct slot pool[POOL_SLOTS];
static int pool_next;
static int pool_live;
static int failures;
#define CHECK(cond) do { if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
failures++; } } while (0)
static void *test_malloc(size_t size)
{
int i;
if (size != sizeof(OSMutex))
{
fprintf(stderr, "FAIL unexpected allocation of %u bytes\n",
(unsigned)size);
exit(1);
}
i = __atomic_fetch_add(&pool_next, 1, __ATOMIC_ACQ_REL);
if (i >= POOL_SLOTS)
return NULL;
if ((uintptr_t)(unsigned)(uintptr_t)&pool[i].os != (uintptr_t)&pool[i].os
|| (uintptr_t)&pool[i].os > 0x7fffffffu)
{
fprintf(stderr, "FAIL pool above 2^31: link at a low base\n");
exit(1);
}
return &pool[i].os;
}
static void test_free(void *p)
{
struct slot *s = (struct slot*)p;
if (!p)
return;
if (s->live)
{
pthread_mutex_destroy(&s->pm);
s->live = 0;
__atomic_fetch_sub(&pool_live, 1, __ATOMIC_ACQ_REL);
}
}
/* Stand-ins for coreinit. OSMutex is recursive. */
void OSInitMutex(OSMutex *mutex)
{
struct slot *s = (struct slot*)mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&s->pm, &attr);
pthread_mutexattr_destroy(&attr);
s->live = 1;
__atomic_fetch_add(&pool_live, 1, __ATOMIC_ACQ_REL);
}
void OSLockMutex(OSMutex *mutex)
{
pthread_mutex_lock(&((struct slot*)mutex)->pm);
}
BOOL OSTryLockMutex(OSMutex *mutex)
{
return pthread_mutex_trylock(&((struct slot*)mutex)->pm) == 0;
}
void OSUnlockMutex(OSMutex *mutex)
{
pthread_mutex_unlock(&((struct slot*)mutex)->pm);
}
#define malloc test_malloc
#define free test_free
#include "../../../wiiu/system/newlib_locks.c"
#undef malloc
#undef free
static int word; /* a newlib static lock: starts at 0 */
static unsigned counter; /* guarded by word */
static pthread_barrier_t start;
static void *hammer(void *arg)
{
int i;
(void)arg;
pthread_barrier_wait(&start);
for (i = 0; i < ROUNDS; i++)
{
unsigned v;
__syscall_lock_acquire_recursive(&word);
v = counter;
counter = v + 1;
__syscall_lock_release_recursive(&word);
}
return NULL;
}
static int try_result;
static void *try_other(void *arg)
{
try_result = __syscall_lock_try_acquire_recursive((int*)arg);
if (try_result == 0)
__syscall_lock_release_recursive((int*)arg);
return NULL;
}
static int try_from_other_thread(int *lock)
{
pthread_t t;
pthread_create(&t, NULL, try_other, lock);
pthread_join(t, NULL);
return try_result;
}
int main(void)
{
pthread_t t[THREADS];
int i;
int rec = 0;
/* Racing first use: one mutex published, the losers' freed. */
pthread_barrier_init(&start, NULL, THREADS);
for (i = 0; i < THREADS; i++)
pthread_create(&t[i], NULL, hammer, NULL);
for (i = 0; i < THREADS; i++)
pthread_join(t[i], NULL);
pthread_barrier_destroy(&start);
CHECK(counter == (unsigned)THREADS * ROUNDS);
CHECK(word != 0);
CHECK(pool_live == 1);
/* Recursive on one thread, excluded from another. */
__syscall_lock_init_recursive(&rec);
CHECK(rec == 0);
__syscall_lock_acquire_recursive(&rec);
CHECK(__syscall_lock_try_acquire_recursive(&rec) == 0);
CHECK(try_from_other_thread(&rec) != 0);
__syscall_lock_release_recursive(&rec);
CHECK(try_from_other_thread(&rec) != 0);
__syscall_lock_release_recursive(&rec);
CHECK(try_from_other_thread(&rec) == 0);
/* Close frees the mutex and returns the word to its initial state;
* the next use creates a new one. */
__syscall_lock_close_recursive(&rec);
CHECK(rec == 0);
CHECK(pool_live == 1);
__syscall_lock_acquire(&rec);
CHECK(rec != 0);
CHECK(pool_live == 2);
__syscall_lock_release(&rec);
__syscall_lock_close(&rec);
__syscall_lock_close(&word);
CHECK(pool_live == 0);
if (failures)
{
fprintf(stderr, "%d failure(s)\n", failures);
return 1;
}
printf("wiiu_newlib_locks: ok\n");
return 0;
}
+1
View File
@@ -17,6 +17,7 @@ IMPORT(OSSavesDone_ReadyToRelease);
IMPORT(OSInitMutex);
IMPORT(OSLockMutex);
IMPORT(OSUnlockMutex);
IMPORT(OSTryLockMutex);
IMPORT(OSInitCond);
IMPORT(OSWaitCond);
IMPORT(OSSignalCond);
+135
View File
@@ -0,0 +1,135 @@
/* RetroArch - A frontend for libretro.
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
/* devkitPPC's newlib guards stdio, the environment, the time zone and
* the atexit lists through weak __syscall_lock_* hooks and does no
* locking when nothing defines them. These give it Cafe OS mutexes,
* so libc state shared between the frontend's threads is serialised.
*
* Its lock word (_LOCK_T, and _LOCK_RECURSIVE_T alike) is a plain int
* that newlib's static locks start at 0, so a mutex is created on
* first use and its address published into the word by compare-and-
* swap. OSMutex is recursive, which serves both kinds of lock. */
#include <stdlib.h>
#include <stdint.h>
#include <wiiu/os/mutex.h>
typedef int wiiu_newlib_lock_t;
static OSMutex *wiiu_newlib_mutex(wiiu_newlib_lock_t *lock)
{
OSMutex *mutex = (OSMutex*)(uintptr_t)
__atomic_load_n(lock, __ATOMIC_ACQUIRE);
int expected = 0;
if (mutex)
return mutex;
if (!(mutex = (OSMutex*)malloc(sizeof(*mutex))))
return NULL;
OSInitMutex(mutex);
if (!__atomic_compare_exchange_n(lock, &expected,
(int)(uintptr_t)mutex, 0,
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
{
/* Another thread published its mutex first. */
free(mutex);
mutex = (OSMutex*)(uintptr_t)expected;
}
return mutex;
}
static void wiiu_newlib_lock_free(wiiu_newlib_lock_t *lock)
{
OSMutex *mutex = (OSMutex*)(uintptr_t)
__atomic_exchange_n(lock, 0, __ATOMIC_ACQ_REL);
free(mutex);
}
static void wiiu_newlib_lock_acquire(wiiu_newlib_lock_t *lock)
{
OSMutex *mutex = wiiu_newlib_mutex(lock);
if (mutex)
OSLockMutex(mutex);
}
static int wiiu_newlib_lock_try_acquire(wiiu_newlib_lock_t *lock)
{
/* newlib's convention: 0 when the lock was taken. */
OSMutex *mutex = wiiu_newlib_mutex(lock);
if (!mutex)
return 0;
return OSTryLockMutex(mutex) ? 0 : 1;
}
static void wiiu_newlib_lock_release(wiiu_newlib_lock_t *lock)
{
OSMutex *mutex = (OSMutex*)(uintptr_t)
__atomic_load_n(lock, __ATOMIC_ACQUIRE);
if (mutex)
OSUnlockMutex(mutex);
}
void __syscall_lock_init(wiiu_newlib_lock_t *lock)
{
*lock = 0;
}
void __syscall_lock_acquire(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_acquire(lock);
}
int __syscall_lock_try_acquire(wiiu_newlib_lock_t *lock)
{
return wiiu_newlib_lock_try_acquire(lock);
}
void __syscall_lock_release(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_release(lock);
}
void __syscall_lock_close(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_free(lock);
}
void __syscall_lock_init_recursive(wiiu_newlib_lock_t *lock)
{
*lock = 0;
}
void __syscall_lock_acquire_recursive(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_acquire(lock);
}
int __syscall_lock_try_acquire_recursive(wiiu_newlib_lock_t *lock)
{
return wiiu_newlib_lock_try_acquire(lock);
}
void __syscall_lock_release_recursive(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_release(lock);
}
void __syscall_lock_close_recursive(wiiu_newlib_lock_t *lock)
{
wiiu_newlib_lock_free(lock);
}