2025-189
16:47:44, atom feed.
As an unintentional follow-up to yesterday's post, I ended up writing a simple bump allocator. The motivation was that I wanted to build a routine to convert relative paths to absolute paths. For this, I need a place to store the absolute path. The relative paths are relative to the folder the game binary sits in, so I can't predict the full string length of the absolute path, meaning I need to dynamically allocate.
Here is the API for the bump allocator; no surprises if you've seen one before.
struct Allocator {
void* base = nullptr;
size_t size = 0;
size_t used = 0;
};
/**
* Set up an allocator. The allocator's memory is guaranteed to be zeroed.
*
* @param a A pointer to an allocator to set up; cannot be `NULL`.
* @param size The number of bytes to allocate. Must be greater than 0.
* @param addr The beginning address to allocate from. Pass `NULL` to defer base
* address selection to the operating system. Note that any other
* value greatly reduces portability.
* @return `false` if setting up the allocator failed for any reason; `true`
* otherwise.
*/
bool allocator_init(Allocator* a, size_t size, void* addr);
/**
* Allocate memory from the allocator.
*
* @param a The allocator to allocate from; cannot be `NULL`.
* @param size The number of bytes to allocate. Must be greater than 0.
* @return If successful, returns a pointer to the beginning of the allocated
* region. If unsuccessful for whatever reason, returns `NULL`.
*/
void* allocator_alloc(Allocator* a, size_t size);
/**
* Reset an allocator, meaning that its memory pool is considered entirely
* unused and the next attempt to allocate will start at the beginning of the
* backing memory pool. This does not deallocate anything from the operating
* system's perspective, but anything previously allocated in the allocator's
* memory pool should be considered invalid after calling this.
*
* @param a The allocator to reset; cannot be `NULL`.
*/
void allocator_reset(Allocator* a);
/**
* Destroy an allocator.
*
* @param a The allocator to destroy; cannot be `NULL`.
* @return `false` if destroying the allocator failed for any reason.
*/
bool allocator_destroy(Allocator* a);
I give the user the option to specify the base address of the memory pool or let the operating system pick it. In production, I'll let the operating system pick it, but in development, I'll control the base address so I can predict the general location of things in memory across program runs.
In the implementation of allocator_init
, I use mmap
instead of malloc
,
like so:
bool allocator_init(Allocator* a, size_t size, void* addr) {
assert(a);
assert(size);
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
if (addr) {
flags |= MAP_FIXED_NOREPLACE;
}
a->base = mmap(addr, size, PROT_READ | PROT_WRITE, flags, -1, 0);
if (!a->base) {
return false;
}
a->size = size;
a->used = 0;
return true;
}
In short, we permit reading and writing on the allocated space, the memory
region is private to our process (MAP_PRIVATE
), we do not use file-backed
memory (MAP_ANONYMOUS
and -1
for the file descriptor parameter), and
should the caller request a specific base address, we tell the operating
system so a fixed address and force failure if our requested address has already
been mapped by us elsewhere in our process (MAP_FIXED_NOREPLACE
).
I'm using this as a frame allocator, meaning that at the beginning of each frame
(at the beginning of each iteration through the game loop), I call
allocator_reset
. I set up the allocator right after initializing SDL near the
top of main
, outside of the game loop. This allows me to use the allocator
before entering the game loop for any one-off scratch work I need to do, and
then immediately upon entering the game loop (with the exception of capturing
the frame start timestamp), the allocator is immediately reset.
Before entering the game loop, I use my relative-path-to-absolute-path utility
to build the full path to the game logic library so that I can check its last
modification time via stat
. I then periodically re-build the full path in the
game loop when I go to re-check the modification time, so in the future, I will
likely also introduce separate, persistent storage to cache such things.
Instead of having to implement my own way to get the base path of the game
binary across operating systems (using convenience functions in Windows and
checking /proc
things in Linux, for example), I can thankfully just leverage
SDL_GetBasePath.
The interface of my routine to build absolute paths looks like this:
char* abs_path(const char* path, Allocator* a);
It takes an allocator to use to obtain storage for the final string. The Jai
language I mentioned in a previous post does something similar for routines that
need to dynamically allocate but passes the current allocator to the function
implicitly in an argument called the context. I don't have that language feature
in C/C++ without approximating via OOP, globals, or function/method partials,
so the allocator is an explicit parameter in my case. I don't like this
signature much, but it accomplishes what I need. I haven't covered this detail
yet, but I have also separated more of the game logic code from the "platform"
and exposed a higher-level game_update
function from the game logic library
to the platform. The game_update
function will likely eventually receive a
handle to the frame allocator so that it can allocate whatever it needs in a
single frame in a manner I can easily control and introspect. That is how
game logic code will also be able to use things like abs_path
without owning
any particular allocator or backing memory pool.
Here are a couple other things I learned along the way. First, it is not only
Linux that has alloca;
Windows
has it
as well. The main difference besides the literal function name is that Linux's
causes undefined behavior on stack overflow whereas Windows's will raise a
structured exception.
I could use this for building strings as I don't anticipate memory required
for paths to be so large that it would cause a problem on the stack. The
second thing I learned is that man pages can even reference books. I noticed
the man page for mmap(2)
(Linux man-pages 6.7, 2023-10-31) referenced
what appeared to be a book, and upon looking it up, indeed, it is "POSIX. 4:
Programming for the Real World," by Gallmeister. Interesting, might want to
skim through it.