# GEBS - the Good Enough Build System GEBS is a reiteration of my previous build system "MIBS" (or MIni Build System). It takes some inspiration from [Tsoding's](https://twitch.tv/tsoding) [nobuild](https://github.com/tsoding/nobuild) and later on [nob.h](https://github.com/tsoding/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](https://github.com/tsoding/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 \`\`\`