Files
aboba/blog/curious-case-of-gebs.md
2025-06-19 23:31:16 +02:00

5.8 KiB

GEBS - the Good Enough Build System

Source code: http://git.kamkow1lair.pl/kamkow1/gebs

GEBS is a reiteration of my previous build system "MIBS" (or MIni Build System). It takes some inspiration from Tsoding's nobuild and later on nob.h. The key difference is the way GEBS is implemented on the inside, which makes it more powerful and extensible than nob.h. GEBS also includes a bunch of extra helper macros, which turn C into a language more akin to Go or Zig, but more on that later.

So what makes GEBS different?

Allocators

So one thing I've noticed is that nob.h is used alongside of arena. If you look into the implementation you can see some things, which are somewhat redundant like `arena_sprintf()` or `arena_da_append()`, `arena_sb_append_cstr()` and so on... First of all, why is an arena library managing string builders and dynamic arrays? In my opinion it should be the other way around. A string builder should rather accept a generic allocator interface, which it can then utilize to get it's memory. Basically we supplement a dynamic structure with an allocator of choice. In GEBS this is done via a `Gebs_Allocator` interface.

```c typedef struct { void *(*malloc)(void *self, size_t size); void (*free)(void *self, void *memory); void *(*realloc)(void *self, void *memory, size_t prev_size, size_t new_size); } Gebs_Allocator;

// Wrapper macros #define gebs_malloc(alloc, size) ((alloc)->malloc((void *)(alloc), (size))) #define gebs_free(alloc, memory) ((alloc)->free((void *)(alloc), (memory))) #define gebs_realloc(alloc, memory, prev_size, new_size)
((alloc)->realloc((void *)(alloc), (memory), (prev_size), (new_size))) ```

We then can implement an allocator that conforms to this interface and it will work with any dynamic structure. This is my version of the `XXX_da_append()` macro:

```c #define gebs_list_append_alloc(alloc, list, item) \ do { \ if ((list)->items == nil) { \ (list)->capacity = 1; \ (list)->items = gebs_malloc((alloc), \ sizeof((list)->items) * (list)->capacity); \ } else { \ if ((list)->count == (list)->capacity) { \ size_t __prev_capacity = (list)->capacity; \ (list)->capacity = 2; \ (list)->items = gebs_realloc((alloc), (list)->items, \ sizeof((list)->items) * __prev_capacity, \ sizeof((list)->items) * (list)->capacity); \ } \ } \ (list)->items[(list)->count++] = (item); \ } while(0)

#define gebs_list_append(list, item) \ gebs_list_append_alloc(&gebs_default_allocator, (list), (item)) ```

This way a dynamic list can work with any kind of allocator - the default libc allocator, an arena or literally anything else. We're not tied to the libc allocator and then have to implement the same macro of all other allocators.

Defer macro

Ever forgot to place a `free()` call on function exit or an `fclose()`? The defer macro comes to the rescue. Here's a short snippet: (Taken straight form the source code of this website btw.)

```c cJSON *root = cJSON_CreateObject(); defer { cJSON_Delete(root); }

char *time = __TIME__;
uchar md5_buf[16];
md5String(time, md5_buf);
String_Builder sb = {0};
defer { sb_free(&sb); }
for (size_t i = 0; i < 16; i++) {
    sb_append_nstr(&sb, fmt("%02x", md5_buf[i]));
}
sb_finish(&sb);

cJSON_AddItemToObject(root, "build_id", cJSON_CreateString(sb.items));

make_application_json(result, 200, root);

```

If not for the `defer { ... }` macro, remebering when to free memory would have been quite hellish. Another example:

```c NString_List env = {0}; defer { list_free(&env); }

String_Builder out = {0};
defer { sb_free(&out); }

char path[PATH_MAX] = {0};
if (!get_baked_resource_path("home.html", path, sizeof(path))) {
    make_internal_server_error(result);
    return;
}

```

On `return` we'd have to NOT FORGET to add `list_free()` and `sb_free()`, but now that we have our defer, we can kind of shut the brain off and not concern ourselves with freeing the memory. We can be 100% sure it's going to be freed if we step into the return statement.

The implementation is quite simple, actually

``` #define defer defer__2(COUNTER) #define defer__2(X) defer__3(X) #define defer__3(X) defer__4(defer__id##X) #define defer__4(ID) auto void ID##func(char (*)[]); attribute((cleanup(ID##func))) char ID##var[0]; void ID##func(char (*ID##param)[]) ```

Source article: https://gustedt.wordpress.com/2025/01/06/simple-defer-ready-to-use/

compile_flags.txt

Clang/LLVM docs: https://clang.llvm.org/docs/JSONCompilationDatabase.html

I use clangd inside of my vim. Clangd can be configured via a json database compile_commands.json. It's quite complicated for GEBS in a sense that it uses the `XX.c -> XX.o` building pattern, while GEBS is focused more on unity builds (it's on the programmer to implement caching). Luckily, clangd can be configured via a simple and minimalistic config file - `compile_flags.txt`, which holds only compiler flags that are used to compile our C files. We can for eg. put some include paths in there and clangd will pick them up.

In GEBS we can generate a `compile_flags.txt` file using a built-in macro:

```c #define CFLAGS \ "-I.", \ "-I./some-lib", \ "-Wall", \ "-Wextra" \

// #define other stuff like CC, LDFLAGS, SOURCES

make_compile_flags(CFLAGS); // Will output the file

```