如何组织 C 项目:这些最佳实践对我很有效

如何组织 C 项目以及选择构建工具,一旦进入真实世界的复杂性,就很快变得充满争议。评论者在传统的基于 Make 的布局(包含 `src/`、`include/` 和手写 Makefile)与现代系统如 Meson、CMake,甚至 Rust 的 Cargo 或 Go 的工具链之间权衡,围绕简单性、灵活性、可移植性以及“零配置”便利性展开讨论。过程中,他们还争论头文件和源文件的组织方式、测试策略、代码生成、交叉编译,以及 C 语言碎片化的工具生态究竟是其普及所必须付出的代价,还是转向更新语言的理由。

整体项目布局

  • 所提结构被认为与 C++ 的“pitchfork”布局(src/、include/ 等)非常相似。
  • 有人认为,将 .c 和 .h 分开到不同顶层目录对于内部头文件来说没有必要;也有人觉得这对平台特定实现很有帮助(例如 platform_windows.c 与 platform_linux.c)。
  • 许多人更喜欢把所有子系统都放在 src/ 下,而只把 include/ 留给公共/库头文件。
  • 对库来说,可安装头文件与内部头文件的区分被认为至关重要。
  • 有些人不喜欢在仓库里放 bin/lib/,而更倾向于使用可配置的 PREFIX(build/$HOME/.local)以及会调整 PATH 的环境文件。

Make、CMake 和替代构建工具

  • 多种灵活 Makefile 的写法:模式规则、wildcard + patsubst,以及自动生成对象文件列表,以避免手动更新 Makefile。
  • 澄清内置的 %.o 规则只有在源文件和对象文件共享同一目录时才有效;可用的变通方法包括 VPATH 或显式规则。
  • 对文章中的 Makefile 的批评:它依赖 -j 下脆弱的构建顺序行为,保存 compile_commands.json,并把编辑器/CI 目录混入“杂物”。
  • 有些人主张对小项目采用极简构建(单个 all.c,通过 #include 包含所有内容);批评者指出这无法很好扩展到 sanitizer、测试、CI。
  • 对 Make 的看法分歧很大:一些人认为它简单、可组合且可扩展;另一些人则认为与 Meson、Buck2、Xmake、Zig 的构建系统等相比,它过时且繁琐。
  • CMake 被认为功能强大但笨拙;关于显式列出文件与使用 glob 的争论不断(涉及重生成和正确性权衡)。

工具、测试和代码生成

  • 建议包括:MinUnit、Clang-Tidy(例如 CERT 配置文件)、Clang-Format、-Weverything、sanitizers(ASan/UBSan),以及严格警告(如 -Werror=missing-declarations 等)。
  • 关于 C 中单元测试的看法不一:有些人强调断言和上下文相关的过程;另一些人则把经过大量测试的 C 项目当作反例。
  • 几个人描述了把单元测试与实现并置的做法,以及像 *_test.c 这样由 Make 自动检测的约定。
  • 强烈鼓励使用代码生成器(Lua、Python、模板引擎,甚至 C 自身),并将 src/ 视为生成器,gen/ 视为生成出的 C,obj/ 视为构建输出。
  • 讨论了单独编译头文件,以及 #pragma once 与 include guard 之间的取舍。

包管理、交叉编译和元讨论

  • 希望有“像 Cargo 一样”的简单性:单一标准工具、零或低配置、一致的布局。
  • 也有人认为,C 的历史悠久、可移植性和多样的平台使得单一标准工具不现实;因此多种包/构建系统(Conan、vcpkg、pkg-config、vendoring)并存。
  • 交叉编译:提到 zig cc 和嵌入式风格工具链;强调通过接口把平台层隔离开来。
  • 围绕 Rust vs C 的长篇元讨论:Rust 的 Cargo 和 Go 的工具链受到称赞;有些人抱怨“重写成 Rust”的跑题言论,另一些人则认为比较工具链是合理且意料之中的。