Skip to content

Instantly share code, notes, and snippets.

@ITotalJustice
Last active October 5, 2024 04:18
Show Gist options
  • Save ITotalJustice/eb9c47d0fdbc34e275b2ca79460bf0e3 to your computer and use it in GitHub Desktop.
Save ITotalJustice/eb9c47d0fdbc34e275b2ca79460bf0e3 to your computer and use it in GitHub Desktop.
mmap for sdl3
// todo:
// - check for flag incompatibilty (truncate + read), (anon + any other flag).
// - create windows version.
// - map fixed address MAP_FIXED.
// NOTES (linux):
// - "offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE)".
// so it's best for the user to just pass 0 and handling offset themselves.
// - offset should be 0 when using MAP_ANONYMOUS.
// - mmap cannot map a write only file, it has to be either read only
// or RW. i currently force the file to be opened as RW if the user
// asks for write only, and then mmap with write only perms
// so, to the user, the same result is achived.
// NOTES:
// - a size of 0 will map the whole address, as long as the file exists.
// otherwise an error is returned.
// - if the file does not exist, then the file is created with size (offset + size).
#include "SDL_memory_map.h"
#include <SDL3/SDL.h>
// #include "SDL_internal.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#ifdef MAP_ANONYMOUS
#define MMAP_ANON MAP_ANONYMOUS
#elif defined(MAP_ANON)
#define MMAP_ANON MAP_ANON
#else
// todo: just disable anon and return error if flag is used.
#error MAP_ANONYMOUS and MAP_ANON not defined.
#endif
struct SDL_MemoryMappedFile {
void* data;
size_t length;
};
typedef struct MapIo {
SDL_MemoryMappedFile* map;
size_t offset;
} MapIo;
static Sint64 SDLCALL map_size(void *userdata)
{
MapIo* io = userdata;
return io->map->length;
}
static Sint64 SDLCALL map_seek(void *userdata, Sint64 offset, SDL_IOWhence whence)
{
MapIo* io = userdata;
Sint64 new_offset = 0;
switch (whence) {
case SDL_IO_SEEK_SET:
new_offset = offset;
break;
case SDL_IO_SEEK_CUR:
new_offset = (Sint64)io->offset + offset;
break;
case SDL_IO_SEEK_END:
new_offset = (Sint64)io->map->length + offset;
break;
default:
SDL_SetError("Unknown value for 'whence'");
return -1;
}
if (new_offset < 0) {
new_offset = 0;
}
if ((size_t)new_offset > io->map->length) {
new_offset = io->map->length;
}
return io->offset = new_offset;
}
static size_t SDLCALL map_read(void *userdata, void *ptr, size_t size, SDL_IOStatus *status)
{
MapIo* io = userdata;
size_t sz_bytes = io->offset + size;
sz_bytes = sz_bytes > io->map->length ? io->map->length - io->offset : sz_bytes;
SDL_memcpy(ptr, (const Uint8*)io->map->data + io->offset, sz_bytes);
if (sz_bytes < size) {
*status = SDL_IO_STATUS_EOF;
}
io->offset += sz_bytes;
return sz_bytes;
}
static size_t SDLCALL map_write(void *userdata, const void *ptr, size_t size, SDL_IOStatus *status)
{
MapIo* io = userdata;
size_t sz_bytes = io->offset + size;
sz_bytes = sz_bytes > io->map->length ? io->map->length - io->offset : sz_bytes;
SDL_memcpy((Uint8*)io->map->data + io->offset, ptr, sz_bytes);
if (sz_bytes < size) {
*status = SDL_IO_STATUS_EOF;
}
io->offset += sz_bytes;
return sz_bytes;
}
static bool SDLCALL map_flush(void *userdata, SDL_IOStatus *status)
{
MapIo* io = userdata;
// forces a sync, will only return when finished
// use async if you are okay with the sync happening in the background.
// i assume most people would want sync, hence calling flush ;)
errno = 0;
if (msync(io->map->data, io->map->length, MS_SYNC)) {
SDL_SetError("failed to msync: %s", strerror(errno));
return false;
}
return true;
}
static bool SDLCALL map_close(void *userdata)
{
MapIo* io = userdata;
if (io) {
SDL_UnmapMemoryMapFile(io->map);
SDL_free(io);
}
return true;
}
SDL_MemoryMappedFile *SDL_MemoryMapFile(const char *file, size_t offset, size_t length, int mode, int flags)
{
int fd = -1;
errno = 0;
SDL_MemoryMappedFile* map = SDL_calloc(1, sizeof(*map));
if (!map) {
goto fail;
}
if (!mode) {
SDL_InvalidParamError("mode");
goto fail;
}
// set open flags.
int open_mode = 0;
if ((mode & SDL_MEMORY_MAP_MODE_RW) == SDL_MEMORY_MAP_MODE_RW) {
open_mode |= O_RDWR;
}
else if (mode & SDL_MEMORY_MAP_MODE_READ) {
open_mode |= O_RDONLY;
}
else if (mode & SDL_MEMORY_MAP_MODE_WRITE) {
// write only files cannot be mmmap'd
open_mode |= O_RDWR;
}
if (flags & SDL_MEMORY_MAP_FLAG_CREATE) {
open_mode |= O_CREAT;
}
if (flags & SDL_MEMORY_MAP_FLAG_TRUNCATE) {
open_mode |= O_TRUNC;
}
// set mmap flags.
int mmap_prot = 0;
int mmap_flags = 0;
if ((mode & SDL_MEMORY_MAP_MODE_RW) == SDL_MEMORY_MAP_MODE_RW) {
mmap_prot |= PROT_READ | PROT_WRITE;
mmap_flags |= MAP_SHARED;
}
else if (mode & SDL_MEMORY_MAP_MODE_READ) {
mmap_prot |= PROT_READ;
mmap_flags |= MAP_PRIVATE;
}
else if (mode & SDL_MEMORY_MAP_MODE_WRITE) {
mmap_prot |= PROT_WRITE;
mmap_flags |= MAP_SHARED;
}
if (mode & SDL_MEMORY_MAP_MODE_EXECUTE) {
mmap_prot |= PROT_EXEC;
}
if (flags & SDL_MEMORY_MAP_FLAG_ANONYMOUS) {
mmap_flags &= ~MAP_SHARED;
mmap_flags |= MMAP_ANON | MAP_PRIVATE;
}
// file size, this is updated below if needed.
size_t size = offset + length;
if (mmap_flags & MMAP_ANON) {
// length needs to be set if anon.
if (!length) {
SDL_InvalidParamError("length");
goto fail;
}
// offset should be zero if annon.
if (offset) {
SDL_InvalidParamError("offset");
goto fail;
}
}
else {
// file needs to be vaild.
if (!file) {
SDL_InvalidParamError("file");
goto fail;
}
// length needs to be set for truncate.
if ((open_mode & O_TRUNC) && !length) {
SDL_InvalidParamError("length");
goto fail;
}
if (offset) {
const long page_size = sysconf(_SC_PAGE_SIZE);
if (page_size <= 0) {
SDL_SetError("Failed to get page size (sysconf(_SC_PAGE_SIZE)): %s", strerror(errno));
goto fail;
}
if (offset % (size_t)page_size) {
SDL_SetError("Offset: %zu is not a multiple of page size: %ld", offset, page_size);
goto fail;
}
}
// todo: what should be the standard permission for this file?
fd = open(file, open_mode, 0644);
if (fd < 0) {
SDL_SetError("Failed to open file: %s %s", file, strerror(errno));
goto fail;
}
// set the new file size.
if (open_mode & O_TRUNC) {
if (ftruncate(fd, size)) {
SDL_SetError("Failed to ftruncate file: %s %s", file, strerror(errno));
goto fail;
}
}
else {
// the file may have just been created, we check this
// by seeing if the file size is 0
struct stat st;
if (fstat(fd, &st)) {
SDL_SetError("Failed to fstat file: %s %s", file, strerror(errno));
goto fail;
}
if (!st.st_size && (open_mode & O_CREAT)) {
if (ftruncate(fd, size)) {
SDL_SetError("Failed to ftruncate file: %s %s", file, strerror(errno));
goto fail;
}
}
else {
size = st.st_size;
}
if (!length) {
length = size;
}
}
}
if (offset >= size) {
SDL_SetError("Offset: %zu exceeds file size: %zu", offset, size);
goto fail;
}
if (length > size) {
SDL_SetError("Length: %zu exceeds file size: %zu", length, size);
goto fail;
}
if (offset + length > size) {
SDL_SetError("Mapped range: %zu exceeds file size: %zu", offset + length, size);
goto fail;
}
map->length = length;
map->data = mmap(NULL, length, mmap_prot, mmap_flags, fd, offset);
if (!map->data || map->data == MAP_FAILED) {
SDL_SetError("Failed to mmap %s", strerror(errno));
goto fail;
}
// note: at this point, the file can safely be closed.
if (fd >= 0) {
close(fd);
}
return map;
fail:
if (fd >= 0) {
close(fd);
}
SDL_UnmapMemoryMapFile(map);
return NULL;
}
void SDL_UnmapMemoryMapFile(SDL_MemoryMappedFile *map)
{
if (map) {
if (map->data && map->data != MAP_FAILED) {
munmap(map->data, map->length);
}
SDL_free(map);
}
}
void *SDL_GetMemoryMapData(SDL_MemoryMappedFile *map, size_t *length)
{
if (!map || !map->data) {
SDL_InvalidParamError("map");
return NULL;
}
if (length) {
*length = map->length;
}
return map->data;
}
SDL_IOStream *SDL_MemoryMapFileIO(const char *file, size_t offset, size_t length, int mode, int flags)
{
// try and allocate before maping (and potentially truncating) the file.
MapIo* io = SDL_calloc(1, sizeof(*io));
if (!io) {
goto fail;
}
io->map = SDL_MemoryMapFile(file, offset, length, mode, flags);
if (!io->map) {
goto fail;
}
SDL_IOStreamInterface iface;
SDL_INIT_INTERFACE(&iface);
iface.size = map_size;
iface.seek = map_seek;
iface.flush = map_flush;
iface.close = map_close;
if (mode & SDL_MEMORY_MAP_MODE_READ) {
iface.read = map_read;
}
if (mode & SDL_MEMORY_MAP_MODE_WRITE) {
iface.write = map_write;
}
SDL_IOStream* stream = SDL_OpenIO(&iface, io);
if (!stream) {
goto fail;
}
return stream;
fail:
if (io) {
SDL_UnmapMemoryMapFile(io->map);
SDL_free(io);
}
return NULL;
}
#ifndef SDL_memory_map_h_
#define SDL_memory_map_h_
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_iostream.h>
#include <SDL3/SDL_begin_code.h>
/* Set up for C function definitions, even when using C++ */
#ifdef __cplusplus
extern "C" {
#endif
/**
* Mode used for SDL_MemoryMapFile().
*
* \since This enum is available since SDL 3.2.0.
*/
typedef enum SDL_MemoryMapMode
{
SDL_MEMORY_MAP_MODE_READ = 1 << 0, /**< Map as read only. */
SDL_MEMORY_MAP_MODE_WRITE = 1 << 1, /**< Map as write only. */
SDL_MEMORY_MAP_MODE_EXECUTE = 1 << 2, /**< Map as executable. */
SDL_MEMORY_MAP_MODE_RW = SDL_MEMORY_MAP_MODE_READ | SDL_MEMORY_MAP_MODE_WRITE,
SDL_MEMORY_MAP_MODE_RWX = SDL_MEMORY_MAP_MODE_RW | SDL_MEMORY_MAP_MODE_EXECUTE,
} SDL_MemoryMapMode;
/**
* Flags used for SDL_MemoryMapFile().
*
* \since This enum is available since SDL 3.2.0.
*/
typedef enum SDL_MemoryMapFlags
{
SDL_MEMORY_MAP_FLAG_NONE = 0, /**< No flags. */
SDL_MEMORY_MAP_FLAG_CREATE = 1 << 0, /**< Create the file if it doesn't exists. */
SDL_MEMORY_MAP_FLAG_TRUNCATE = 1 << 1, /**< Truncate the file upon opening. */
SDL_MEMORY_MAP_FLAG_ANONYMOUS = 1 << 2, /**< The param is ignored and the mapped area resides in memory. */
} SDL_MemoryMapFlags;
/**
* The mapped file structure.
*
* This operates as an opaque handle.
*
* \since This struct is available since SDL 3.2.0.
*/
typedef struct SDL_MemoryMappedFile SDL_MemoryMappedFile;
/**
* Maps a file to memory.
*
* \param file the file to open.
* This param is ignored if SDL_MEMORY_MAP_FLAG_ANONYMOUS is set.
* \param offset offset of the area to map, must be a multiple of pagesize.
* The offset must be a multiple of OS pagesize, otherwise this
* function will fail.
* It is recommened to pass a value of 0 for the offset, and handle
* offset in your own code.
* If offset exceeds the file size, then an error is returned.
* This function will fail if offset or offset+length exceeds the file size.
* \param length length of the area to map.
* The length can be 0, in which case, the entire file is mapped.
* If the file does not exist, then length must be set to the size
* the file should be created with.
* If SDL_MEMORY_MAP_FLAG_TRUNCATE is set, then length must be set.
* If SDL_MEMORY_MAP_FLAG_ANONYMOUS is set, then length must be set.
* This function will fail if length or offset+length exceeds the file size.
* \param mode open mode, SDL_MemoryMapMode.
* \param flags open flags, SDL_MemoryMapFlags.
* \returns an opaque pointer to SDL_MemoryMappedFile or NULL on failure; call SDL_GetError() for more information.
*
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_MemoryMapMode
* \sa SDL_MemoryMapFlags
*/
extern SDL_DECLSPEC SDL_MemoryMappedFile * SDLCALL SDL_MemoryMapFile(const char *file, size_t offset, size_t length, int mode, int flags);
/**
* Un-mamps mapped memory opened with SDL_MemoryMapFile().
*
* \param map opened using SDL_MemoryMapFile().
*
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_MemoryMapFile
*/
extern SDL_DECLSPEC void SDLCALL SDL_UnmapMemoryMapFile(SDL_MemoryMappedFile *map);
/**
*
*
* \param map opened using SDL_MemoryMapFile().
* \param length the length of the area returned, can be NULL.
* \returns a pointer to the mapped data.
*
* \since This function is available since SDL 3.2.0.
*/
extern SDL_DECLSPEC void * SDLCALL SDL_GetMemoryMapData(SDL_MemoryMappedFile *map, size_t *length);
/**
* Creates an IOStream on the mapped file, internally calls SDL_MemoryMapFile().
*
* \param file the file to open.
* \param offset offset of the area to map, must be a multiple of pagesize.
* \param length length of the area to map.
* \param mode open mode, SDL_MemoryMapMode.
* \param flags open flags, SDL_MemoryMapFlags.
* \returns a pointer to the SDL_IOStream structure that is created or NULL on
* failure; call SDL_GetError() for more information.
*
* \since This function is available since SDL 3.2.0.
*/
extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_MemoryMapFileIO(const char *file, size_t offset, size_t length, int mode, int flags);
/* Ends C function definitions when using C++ */
#ifdef __cplusplus
}
#endif
#include <SDL3/SDL_close_code.h>
#endif /* SDL_memory_map_h_ */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment