How to Structure C Projects: These Best Practices Worked for Me
How to structure C projects and choose build tools quickly turns contentious once real-world complexity enters the picture. Commenters weigh traditional Make-based layouts (with `src/`, `include/`, and hand-written Makefiles) against modern systems like Meson, CMake, and even Rust’s Cargo or Go’s tooling, arguing over trade‑offs between simplicity, flexibility, portability, and “zero‑configuration” convenience. Along the way they debate header and source organization, testing strategies, code generation, cross‑compilation, and whether C’s fragmented tooling ecosystem is an acceptable price for its ubiquity or a reason to favor newer languages.
Overall project layout
- Proposed structure seen as very similar to the C++ “pitchfork” layout (src/, include/, etc.).
- Some argue separating .c and .h into different top-level dirs is unnecessary for internal headers; others find it helpful for platform-specific implementations (e.g., platform_windows.c vs platform_linux.c).
- Several prefer putting all subsystems under
src/, and reservinginclude/only for public/library headers. - Distinction between installable headers vs internal headers is considered crucial for libraries.
- Some dislike
bin/andlib/inside the repo, preferring a configurable PREFIX (build/or$HOME/.local) and env files that adjust PATH.
Make, CMake, and alternative build tools
- Multiple recipes for flexible Makefiles: pattern rules,
wildcard+patsubst, and auto-generated object lists to avoid manually updating Makefiles. - Clarification that built-in
%.orules only work when sources and objects share a directory; workarounds include VPATH or explicit rules. - Critique of the article’s Makefile: relies on fragile build-order behavior under
-j, storescompile_commands.json, and mixes in editor/CI directories as “litter”. - Some advocate very minimal builds (single
all.cthat#includes everything) for small projects; critics note this doesn’t scale to sanitizers, tests, CI. - Opinions diverge on Make: viewed by some as simple, composable, and scalable; by others as outdated and fiddly compared to Meson, Buck2, Xmake, Zig’s build system, etc.
- CMake is seen as powerful but awkward; debate around listing files explicitly vs using globs (with regeneration and correctness tradeoffs).
Tooling, testing, and code generation
- Suggestions: MinUnit, Clang-Tidy (e.g., CERT profile), Clang-Format,
-Weverything, sanitizers (ASan/UBSan), and strict warnings (-Werror=missing-declarations, etc.). - Mixed views on unit testing in C: some emphasize assertions and context-sensitive procedures; others point to heavily tested C projects as counterexamples.
- Several describe co-locating unit tests with implementation, plus conventions like
*_test.cauto-detected by Make. - Strong encouragement to use code generators (Lua, Python, templating engines, even C itself) and treat
src/as generators,gen/as emitted C,obj/as build output. - Discussion of compiling headers alone and the
#pragma oncevs include-guard tradeoff.
Packages, cross-compilation, and meta-debate
- Desire for “Cargo-like” simplicity: single standard tool, zero/low configuration, consistent layout.
- Others argue C’s age, portability, and diverse platforms make a single standard tool unrealistic; multiple package/build systems (Conan, vcpkg, pkg-config, vendoring) coexist instead.
- Cross-compilation: mention of
zig ccand embedded-style toolchains; emphasis on separating platform layer behind interfaces. - Lengthy meta-thread on Rust vs C: Rust’s Cargo and Go’s tooling praised; some complain about “rewrite in Rust” derailments, others say comparing tooling is fair and expected.