Skip to content
yuozas pixel-art Yuozas in his pink sweater

games · pixels · music · code — from Vilnius with love

← back to the shelf

monorepo-of-micro-libraries.mdx

A Monorepo of Micro-Libraries

A CMake workspace that behaves like a .sln: drop a lib_ folder in and everything wires up — engine-agnostic C++ cores with thin C shims, and every claim on this page compiles.

2026-07-21 cppcmakearchitecturegame-devreceipts

I spend my days in .NET, where the solution file is a quiet luxury: open the .sln, and every project is just there — building, testing, referencing each other, no ritual. Then I’d switch to C++ for game work in the evening and feel like I’d traveled back twenty years. So I built the thing I was missing: a CMake workspace that behaves like a solution file. Drop a lib_* folder in the root, and it’s part of the build. Delete it, and it isn’t. That’s the whole pitch.

This page is the first in a series pulled from something I wrote for myself called the Game Development Bible — and, in keeping with how I do scripture, nothing here is preached that doesn’t compile. The receipt is public: github.com/Yuozas/game-dev-toolkit — clone it, run three commands, watch the tests pass on your machine, not mine.

The shape: libraries first, engines last

Every library in the workspace is built to outlive whatever engine consumes it. The rule has two halves:

  1. A static C++ core (libinventory.a) — pure domain logic, no engine headers, no engine allocators, no engine anything.
  2. A thin C shim (libinventory_c.so) — opaque handles, structured errors, out-params. Exceptions never cross the ABI. This is the door every engine can open: Unity through P/Invoke, Unreal through its build system, Godot through GDExtension — same .so, same contract.

Why so paranoid about the boundary? Because I’ve watched the other version of this story: engine-coupled code where coming back after two months of break means relearning the whole system from zero. I want my libraries to read like APIs — uniform, standard, boring at the seams — so the interesting parts can afford to be interesting.

The .sln feeling, implemented

The root CMakeLists.txt doesn’t list libraries. It discovers them:

file(GLOB _gdt_lib_dirs RELATIVE "${CMAKE_SOURCE_DIR}" "lib_*")
foreach(_lib_dir IN LISTS _gdt_lib_dirs)
    # lib_core always goes first; everything else joins if it has a CMakeLists
    ...
    add_subdirectory(${_lib_dir})
endforeach()

Two conveniences ride on top:

  • GDT_EXCLUDE_LIBS — a cache variable holding lib_* names to skip. A library that’s mid-surgery gets benched with one flag, not with commented-out lines that rot in the root file. The configure log says Skipping excluded library: lib_foo so the bench is visible, never silent.
  • gdt_add_library(<name> [WITH_C_API]) — one call scaffolds lib_<name>/include, src, test, and optionally the C bindings dir, wired into the same discovery loop. New library, zero ceremony.

Tests follow the same discipline through ctest labels — unit, property, performance, integration, stress — so ctest --preset test-all-debug runs the world, and a label filter runs exactly the slice you’re iterating on.

The receipt, and what it cost

The covenant on this site is that “verified” and “should” are different words. So here’s the verification, exact:

  • Cold configure + build on Linux (GCC 13, Ninja): clean.
  • ctest: 100% tests passed, 0 tests failed out of 8 — unit, property, performance suites and a pure-C test that exercises the shim through the C surface only.
  • Both C example consumers compile in the default target and run — including the one that deliberately triggers a domain error to show a structured error crossing the ABI intact instead of an exception detonating on it.

And because a receipt you didn’t audit is just a screenshot: when I audited this workspace before publishing, it was not clean. Two landmines were live in lib_core:

  • Result::Value() returned a shared mutable static on the error path — and the #ifdef DEBUG assert that was supposed to catch misuse was dead code in every build type, because DEBUG is not the opposite of NDEBUG. Every configuration shipped the dangerous path, silently. It now fails fast in all builds, with a death test proving it, and a ValueOr(fallback) for callers who genuinely want a default.
  • MakeUnique leaked by design — it release()d into a non-owning wrapper. Guaranteed leak at every would-be call site; there were zero call sites, which is the only reason it wasn’t a fire. It now returns an owning pointer, with an instance-counting test standing guard.

I’m telling you this instead of hiding it because the audit is the product. A workspace that claims discipline should be able to show you the day its own discipline caught it.

What this page does not claim

The original Bible document this came from also preached Conan packaging, a Python C-API generator, Docker build environments, and per-library watch loops. None of that exists in the repo, so none of it is claimed here. Roadmap and receipt are different shelves. This page only stocks the second one.

Where this goes next

The C shim earned its own story: fixing it surfaced six distinct defects, three of them latent ABI bugs that would have shipped quietly — dangling error strings, error codes reported as success, exceptions with a free pass across the boundary. That post is “One Library, Every Engine”, and it’s next on this shelf.

claims that compile. everything else is paint.

← back to the shelf

every claim checkable — that's the covenant

run.exe
  • go: home ✦
  • go: now
  • go: the shelf
  • go: creative corner
  • go: about me
  • go: cv (for the serious)
  • go: the 2020 site
  • open: a random project
  • do: copy email
  • do: print / download cv
  • do: sparkle burst
  • do: sign the guestbook