mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
memmap: shared memory mapped at more than one address, and a JIT write toggle
Two things a core with a fastmem window needs from the host that memmap did not offer, both taken from what such a core had to carry itself. memshm_create, memshm_map, memshm_unmap and memshm_destroy: an anonymous-backed region that can be mapped at several addresses, which is how a guest RAM is mirrored into a large reserved window. Windows uses a pagefile-backed file mapping and MapViewOfFileEx at the hint; Android a memfd, since Bionic has no shm_open; everything else shm_open with the name unlinked at once, so it never outlives the process. The hint is never MAP_FIXED -- that silently replaces a neighbour -- and a caller that wanted a particular address compares the result. The fd rides in the handle as fd + 1, so the encoding never depends on 0 being stdin. On the consoles with no mman every call reports NULL. memjit_write_begin and memjit_write_end: on Apple Silicon, where MAP_JIT pages are write-xor-execute per thread, they switch the calling thread to writing and back with pthread_jit_write_protect_np, depth-counted per thread so one thread's nesting never flips another's pages. Everywhere else they are empty. The test maps one region twice, writes a pattern through the first address and reads it through the second, changes a word through the second and sees it through the first, then unmaps both and destroys the handle. memjit is called nested and returns. C89 -pedantic warning-free on Linux and both Win32 targets at 0x0400, the test builds for Windows, ASan and UBSan are clean, and it joins Makefile.test and the samples workflow.
This commit is contained in:
1 parent
e17420a13d
commit
73462ad10a
6 files changed
+323
No files matched your search
@@ -170,6 +170,7 @@ jobs:
|
||||
retro_procbarrier_test
|
||||
retro_asym_eventcount_test
|
||||
sthread_ex_test
|
||||
memshm_test
|
||||
msg_fifo_queue_test
|
||||
audio_mixer_loop_test
|
||||
audio_mixer_rate_guard_test
|
||||
|
||||
@@ -253,6 +253,11 @@ threads-samples:
|
||||
$(MAKE) -C samples/rthreads/sthread_ex_test clean
|
||||
$(MAKE) -C samples/rthreads/sthread_ex_test check SANITIZER=$(CONCURRENCY_SAN)
|
||||
$(MAKE) -C samples/rthreads/sthread_ex_test clean
|
||||
# shared memory mapped twice is one memory: written through one
|
||||
# address, read through the other
|
||||
$(MAKE) -C samples/memmap/memshm_test clean
|
||||
$(MAKE) -C samples/memmap/memshm_test check SANITIZER=$(CONCURRENCY_SAN)
|
||||
$(MAKE) -C samples/memmap/memshm_test clean
|
||||
# same, and the only place the queue and the eventcount are
|
||||
# exercised against each other
|
||||
$(MAKE) -C samples/queues/retro_waitable_spsc_test clean
|
||||
|
||||
@@ -170,4 +170,62 @@ void memdecommit(void *addr, size_t len, bool strict);
|
||||
*/
|
||||
void memrelease(void *addr, size_t len);
|
||||
|
||||
/**
|
||||
* memshm_create:
|
||||
* @name : a name unique to this process, e.g. built from the pid.
|
||||
* @len : size in bytes.
|
||||
*
|
||||
* Creates an anonymous-backed shared memory region that can be mapped
|
||||
* at more than one address with memshm_map -- how an emulator mirrors
|
||||
* a guest RAM into several places in a large reserved window. The name
|
||||
* is unlinked at once where the platform links it, so it never outlives
|
||||
* the process. Windows uses a pagefile-backed file mapping, Android a
|
||||
* memfd (Bionic has no shm_open), everything else shm_open.
|
||||
*
|
||||
* Returns: an opaque handle for memshm_map / memshm_destroy, or NULL.
|
||||
* On platforms with no mman this is always NULL.
|
||||
*/
|
||||
void *memshm_create(const char *name, size_t len);
|
||||
|
||||
/**
|
||||
* memshm_destroy:
|
||||
* @handle : from memshm_create.
|
||||
*
|
||||
* Closes the handle. Mappings made from it stay valid until unmapped.
|
||||
*/
|
||||
void memshm_destroy(void *handle);
|
||||
|
||||
/**
|
||||
* memshm_map:
|
||||
* @handle : from memshm_create.
|
||||
* @offset : byte offset into the region; page-aligned.
|
||||
* @hint : preferred address, or NULL. A hint, never forced: on a
|
||||
* taken address the platform picks another, and the
|
||||
* caller compares the result against what it asked for.
|
||||
* @len : bytes to map.
|
||||
* @prot : PROT_READ | PROT_WRITE | PROT_EXEC as memmap.h defines.
|
||||
*
|
||||
* Returns: the mapped address, or NULL.
|
||||
*/
|
||||
void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot);
|
||||
|
||||
/**
|
||||
* memshm_unmap:
|
||||
* @addr : from memshm_map.
|
||||
* @len : the length passed to memshm_map.
|
||||
*/
|
||||
void memshm_unmap(void *addr, size_t len);
|
||||
|
||||
/**
|
||||
* memjit_write_begin / memjit_write_end:
|
||||
*
|
||||
* On a platform whose JIT pages are write-xor-execute per thread --
|
||||
* Apple Silicon, where MAP_JIT memory is executable until the thread
|
||||
* asks otherwise -- these switch the calling thread to writing and back
|
||||
* (pthread_jit_write_protect_np). Nesting is counted, so a writer that
|
||||
* calls another writer is fine. Everywhere else they do nothing.
|
||||
*/
|
||||
void memjit_write_begin(void);
|
||||
void memjit_write_end(void);
|
||||
|
||||
#endif
|
||||
@@ -400,3 +400,158 @@ void memrelease(void *addr, size_t len)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Named shared memory, mappable at more than one address */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#if defined(_WIN32) && !defined(_XBOX)
|
||||
|
||||
void *memshm_create(const char *name, size_t len)
|
||||
{
|
||||
HANDLE h;
|
||||
wchar_t wname[128];
|
||||
int i;
|
||||
/* The name is ASCII by contract; widen it byte-for-byte. */
|
||||
for (i = 0; i < 127 && name[i]; i++)
|
||||
wname[i] = (wchar_t)(unsigned char)name[i];
|
||||
wname[i] = 0;
|
||||
h = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
|
||||
(DWORD)((uint64_t)len >> 32), (DWORD)(len & 0xFFFFFFFFu), wname);
|
||||
return (h == NULL) ? NULL : (void*)h;
|
||||
}
|
||||
|
||||
void memshm_destroy(void *handle)
|
||||
{
|
||||
if (handle)
|
||||
CloseHandle((HANDLE)handle);
|
||||
}
|
||||
|
||||
void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot)
|
||||
{
|
||||
DWORD access = FILE_MAP_READ;
|
||||
void *p;
|
||||
if (prot & PROT_WRITE)
|
||||
access |= FILE_MAP_WRITE;
|
||||
if (prot & PROT_EXEC)
|
||||
access |= FILE_MAP_EXECUTE;
|
||||
p = MapViewOfFileEx((HANDLE)handle, access,
|
||||
(DWORD)((uint64_t)offset >> 32), (DWORD)(offset & 0xFFFFFFFFu), len, hint);
|
||||
if (!p && hint)
|
||||
p = MapViewOfFileEx((HANDLE)handle, access,
|
||||
(DWORD)((uint64_t)offset >> 32), (DWORD)(offset & 0xFFFFFFFFu), len, NULL);
|
||||
return p;
|
||||
}
|
||||
|
||||
void memshm_unmap(void *addr, size_t len)
|
||||
{
|
||||
(void)len;
|
||||
if (addr)
|
||||
UnmapViewOfFile(addr);
|
||||
}
|
||||
|
||||
#elif defined(HAVE_MMAN) && !defined(__EMSCRIPTEN__)
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
/* The handle is the file descriptor, carried in the pointer. */
|
||||
#define MEMSHM_FD(h) ((int)(intptr_t)(h))
|
||||
#define MEMSHM_H(fd) ((void*)(intptr_t)(fd))
|
||||
|
||||
void *memshm_create(const char *name, size_t len)
|
||||
{
|
||||
int fd;
|
||||
#if defined(__ANDROID__)
|
||||
/* Bionic has no shm_open. A memfd is anonymous and needs no name in
|
||||
* the filesystem, so nothing to unlink. */
|
||||
fd = (int)syscall(__NR_memfd_create, name, 1u /* MFD_CLOEXEC */);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
#else
|
||||
fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
if (fd < 0)
|
||||
return NULL;
|
||||
/* Unlink at once: the fd keeps the object alive, the name does not
|
||||
* outlive the process. */
|
||||
shm_unlink(name);
|
||||
#endif
|
||||
if (ftruncate(fd, (off_t)len) < 0)
|
||||
{
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
/* fd 0 would read as NULL; it cannot be, since 0 is stdin, but the
|
||||
* handle is "fd + 1" so the encoding never depends on that. */
|
||||
return MEMSHM_H(fd + 1);
|
||||
}
|
||||
|
||||
void memshm_destroy(void *handle)
|
||||
{
|
||||
if (handle)
|
||||
close(MEMSHM_FD(handle) - 1);
|
||||
}
|
||||
|
||||
void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot)
|
||||
{
|
||||
void *p;
|
||||
if (!handle)
|
||||
return NULL;
|
||||
/* A hint, never MAP_FIXED: MAP_FIXED silently replaces whatever is
|
||||
* there, and a caller that wanted an address it did not get should
|
||||
* find out by comparing, not by corrupting a neighbour. */
|
||||
p = mmap(hint, len, prot, MAP_SHARED, MEMSHM_FD(handle) - 1, (off_t)offset);
|
||||
return (p == MAP_FAILED) ? NULL : p;
|
||||
}
|
||||
|
||||
void memshm_unmap(void *addr, size_t len)
|
||||
{
|
||||
if (addr)
|
||||
munmap(addr, len);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void *memshm_create(const char *name, size_t len)
|
||||
{
|
||||
(void)name; (void)len;
|
||||
return NULL;
|
||||
}
|
||||
void memshm_destroy(void *handle) { (void)handle; }
|
||||
void *memshm_map(void *handle, size_t offset, void *hint, size_t len, int prot)
|
||||
{
|
||||
(void)handle; (void)offset; (void)hint; (void)len; (void)prot;
|
||||
return NULL;
|
||||
}
|
||||
void memshm_unmap(void *addr, size_t len) { (void)addr; (void)len; }
|
||||
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* JIT write toggle for per-thread W^X */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#if defined(__APPLE__) && defined(__aarch64__)
|
||||
#include <pthread.h>
|
||||
/* pthread_jit_write_protect_np is per thread, and so is this depth:
|
||||
* one thread's nesting must not flip another's pages. */
|
||||
static __thread int memjit_depth;
|
||||
|
||||
void memjit_write_begin(void)
|
||||
{
|
||||
if (memjit_depth++ == 0)
|
||||
pthread_jit_write_protect_np(0);
|
||||
}
|
||||
|
||||
void memjit_write_end(void)
|
||||
{
|
||||
if (--memjit_depth == 0)
|
||||
pthread_jit_write_protect_np(1);
|
||||
}
|
||||
#else
|
||||
void memjit_write_begin(void) { }
|
||||
void memjit_write_end(void) { }
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
TARGET := memshm_test
|
||||
LIBRETRO_COMM_DIR := ../../..
|
||||
SOURCES := memshm_test.c $(LIBRETRO_COMM_DIR)/memmap/memmap.c
|
||||
CFLAGS += -Wall -W -std=gnu99 -g -O1 -I$(LIBRETRO_COMM_DIR)/include
|
||||
LDFLAGS += -lrt
|
||||
OBJS := $(SOURCES:.c=.o)
|
||||
ifneq ($(SAN),)
|
||||
SANITIZER := $(SAN)
|
||||
endif
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
all: $(TARGET)
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
check: all
|
||||
./$(TARGET)
|
||||
clean:
|
||||
rm -f $(OBJS) $(TARGET)
|
||||
.PHONY: all check clean
|
||||
@@ -0,0 +1,81 @@
|
||||
/* memshm_test.c - shared memory mapped twice is the same memory.
|
||||
*
|
||||
* The reason memshm exists is a guest RAM that has to appear at more
|
||||
* than one host address. So the test is the aliasing: one region, two
|
||||
* mappings, a pattern written through the first read back through the
|
||||
* second, and a change through the second seen through the first. Then
|
||||
* both are unmapped, the handle destroyed, and a third mapping from the
|
||||
* destroyed handle must fail -- the fd is closed, not leaked.
|
||||
*
|
||||
* memjit_write_begin/end are called nested; they do nothing off Apple
|
||||
* Silicon and must return, which is all that can be checked here.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <memmap.h>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const size_t len = 1u << 20; /* 1 MiB */
|
||||
char name[64];
|
||||
void *h, *a, *b;
|
||||
volatile uint32_t *pa, *pb;
|
||||
size_t i;
|
||||
int ok = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
printf("memshm\n");
|
||||
|
||||
#if defined(_WIN32)
|
||||
sprintf(name, "memshm_test_%lu", (unsigned long)GetCurrentProcessId());
|
||||
#else
|
||||
sprintf(name, "/memshm_test_%lu", (unsigned long)getpid());
|
||||
#endif
|
||||
|
||||
h = memshm_create(name, len);
|
||||
if (!h)
|
||||
{
|
||||
printf(" memshm_create: not available on this platform; nothing to test\n");
|
||||
printf("memshm: ok (skipped)\n");
|
||||
return 0;
|
||||
}
|
||||
a = memshm_map(h, 0, NULL, len, PROT_READ | PROT_WRITE);
|
||||
b = memshm_map(h, 0, NULL, len, PROT_READ | PROT_WRITE);
|
||||
if (!a || !b || a == b)
|
||||
{
|
||||
printf(" FAIL: two mappings: %p %p\n", a, b);
|
||||
return 1;
|
||||
}
|
||||
pa = (volatile uint32_t*)a; pb = (volatile uint32_t*)b;
|
||||
|
||||
for (i = 0; i < len / 4; i++)
|
||||
pa[i] = (uint32_t)i * 2654435761u;
|
||||
for (i = 0; i < len / 4; i++)
|
||||
if (pb[i] != (uint32_t)i * 2654435761u) { ok = 0; break; }
|
||||
printf(" %s: %zu words written through A read back through B\n", ok ? "alias" : "FAIL", len / 4);
|
||||
|
||||
pb[12345] = 0xDEADBEEFu;
|
||||
if (pa[12345] != 0xDEADBEEFu) { printf(" FAIL: write through B not seen through A\n"); ok = 0; }
|
||||
else printf(" alias: a write through B seen through A\n");
|
||||
|
||||
memjit_write_begin();
|
||||
memjit_write_begin();
|
||||
memjit_write_end();
|
||||
memjit_write_end();
|
||||
printf(" memjit: nested begin/end returned\n");
|
||||
|
||||
memshm_unmap(a, len);
|
||||
memshm_unmap(b, len);
|
||||
memshm_destroy(h);
|
||||
printf(" unmapped both, handle destroyed\n");
|
||||
|
||||
printf(ok ? "memshm: ok\n" : "memshm: FAILED\n");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user