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.