Stack conventions
C (CMake)
Modern C (C11/C17) libraries, services and system software built with CMake, tested with CTest, under sanitizers and static analysis.
Modern C (C11/C17) libraries, services and system software built with CMake, tested with CTest, under sanitizers and static analysis.
Used for: c-cmake.
Principles
- Write standard C (C11 at least, C17 where the toolchain allows) with the standard set per target (C_STANDARD) and compiler extensions off unless the project needs them.
- Follow the SEI CERT C rules - no undefined behaviour, no unchecked conversions, no ignored error returns.
- Make ownership explicit - every allocation has one documented owner and one release path (a create/destroy pair per type).
- Keep functions small with a single exit for cleanup (goto cleanup is acceptable for releasing resources in reverse order).
- Validate every external input at the boundary; check sizes before every copy and use bounded functions (snprintf, memcpy with checked lengths).
- Prefer opaque types (struct declared in the header, defined in the .c file) for anything with invariants.
- Build and test with warnings as errors (-Wall -Wextra -Wpedantic -Werror) and with AddressSanitizer and UndefinedBehaviorSanitizer in CI.
- Use modern target-based CMake - target_* commands with usage requirements, never global include_directories or CMAKE_C_FLAGS edits.
- Keep headers minimal and self-contained, with include guards and only the declarations callers need.
Conventions
Accessibility
Concurrency
- Protect shared data with one mutex per invariant (pthread or C11 threads) and keep critical sections short.
- Use C11 atomics (stdatomic.h) for flags and counters; document memory ordering when not using the default.
- Make signal handlers async-signal-safe - set a volatile sig_atomic_t flag and return.
- Run ThreadSanitizer on threaded tests.
Dependencies
- Manage third-party code with vcpkg (manifest mode), Conan or FetchContent/find_package; never copy sources into the tree without a record of version and licence.
- Link dependencies as imported targets (find_package(ZLIB REQUIRED); target_link_libraries(app PRIVATE ZLIB::ZLIB)).
Documentation
- Document every public function in its header (Doxygen style) - ownership of arguments and results, error returns, thread safety.
- Keep the build instructions (cmake --preset ...) in README current.
Errors
- Check the return value of every call that can fail (malloc, fopen, read, write, snprintf, pthread_*); never ignore it silently.
- Report errors as an enum status or a negative errno; use errno only immediately after the call that set it.
- On failure, release what was acquired and leave out-parameters untouched or in a documented state.
Logging
- Log through one small logging module with levels, not scattered printf; write structured lines (JSON) to stderr or stdout for services.
- Never log secrets or whole buffers of untrusted input; bound what is logged.
Naming
- Prefix every public symbol with the module or library name (net_socket_open, json_parse) - C has one global namespace.
- Use snake_case for functions and variables, ALL_CAPS for macros and enumerators; keep macro use to constants and include guards.
- Make file-local functions and data static.
Performance
- Measure before optimising (perf, gprof, benchmarks); keep data contiguous and access it sequentially.
- Avoid allocation in hot paths; reuse buffers with a documented lifetime.
- Use const and restrict where they are true so the compiler can optimise; mark small hot functions static inline.
Security
- Never use gets, strcpy, strcat, sprintf or unbounded scanf; use snprintf and length-checked memcpy, and check for truncation.
- Check every size computation for overflow before allocating (count and element size multiplied); use calloc or a checked multiply.
- Enable compiler hardening (-D_FORTIFY_SOURCE=3 with optimisation, -fstack-protector-strong, -fstack-clash-protection, PIE, full RELRO).
- Initialise every variable; never read memory after free; set freed pointers to NULL when they stay in scope.
- Validate sizes, ranges and encodings of all external input before use; reject rather than truncate.
- Keep secrets out of source and binaries; read them from the environment or a secret store at run time, never log them, and clear them from memory with explicit_bzero or memset_s when done.
- Never pass external input to system() or popen(); use execve or posix_spawn with an argument vector (command injection).
- Keep dependencies patched; pin them through the package manager (vcpkg manifest, Conan lockfile) or FetchContent with exact tags or hashes.
Structure
- Public headers under include/
/, sources under src/, tests under tests/; one CMake target per library. - Every header has an include guard and compiles on its own; declare in headers, define in .c files.
- Use CMakePresets.json for configure/build/test presets so developers and CI share settings (including a sanitizer preset).
- Require a modern CMake minimum (cmake_minimum_required(VERSION 3.20) or newer) and set C_STANDARD per target.
Testing
- Register tests with CTest (add_test); a plain test executable that returns non-zero on failure is enough, Unity or cmocka when tests grow.
- Run the test suite under AddressSanitizer and UndefinedBehaviorSanitizer in CI; ThreadSanitizer for threaded code; Valgrind where sanitizers cannot run.
- Fuzz parsers and decoders with libFuzzer or AFL++ where input is untrusted.
- Unit tests cover each module's functions in isolation; integration tests run the built executables through CTest with real files and processes.
- Name tests after the behaviour (
parser_rejects_truncated_header), one behaviour per test. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Does every allocation have one owner and exactly one release on every path, including failures?
- Is the return value of every fallible call checked and propagated?
- Are all buffer copies bounded, sizes checked for overflow and truncation handled?
- Is there any undefined behaviour risk (uninitialised variables, use after free, out-of-bounds, signed overflow)?
- Does the change compile warning-free with -Wall -Wextra -Wpedantic -Werror and pass clang-tidy and cppcheck?
- Do tests pass under AddressSanitizer and UndefinedBehaviorSanitizer?
- Are public symbols prefixed and file-local symbols static?
- Is CMake target-based (target_* commands, PUBLIC/PRIVATE usage requirements), with no global flags?
- Is external input validated at the boundary and never passed to system() or a format string?
- Are shared data protected and signal handlers async-signal-safe?
- Are headers self-contained, guarded and documented (ownership, errors, thread safety)?
- Are there tests registered with CTest for new behaviour?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
clang-format |
format | clang-format | no |
clang-tidy |
static | clang-tidy | no |
cppcheck |
static | cppcheck | no |
sanitizer-tests |
test | ctest (ASan/UBSan) | no |
osv-scanner |
security | osv-scanner | no |
gitleaks |
security | gitleaks | no |