143 lines
6.4 KiB
Markdown
143 lines
6.4 KiB
Markdown
---
|
|
title: "My C Project Setup"
|
|
date: 2024-03-28
|
|
---
|
|
|
|
For the last couple of months most of my projects have revolved around
|
|
low-level C programming, with the most prominent one being
|
|
[Lander](https://git.rustybever.be/Chewing_Bever/lander), my URL shortener.
|
|
During this time I've developed a method for structuring my repositories in a
|
|
way that works for me and my development style. In this post, I'll be detailing
|
|
my approach!
|
|
|
|
If you prefer looking at the structure directly, the basic structure's
|
|
available as [a template](https://git.rustybever.be/Chewing_Bever/c-template)
|
|
on my Gitea.
|
|
|
|
## Basic structure
|
|
|
|
The basic structure for my repositories looks like this:
|
|
|
|
```
|
|
.
|
|
├── example
|
|
├── include
|
|
│ └── project_name
|
|
├── src
|
|
│ ├── _include
|
|
│ │ └── project_name
|
|
│ └── project_name
|
|
└── test
|
|
```
|
|
|
|
Let's break it down.
|
|
|
|
Naturally, `src` contains the actual source files, both those native to the
|
|
project and those included from thirdparty libraries. `src/project_name`
|
|
contains all source files native to the project, while thirdparty files are
|
|
stored in their own subdirectories separated by library, or directly in `src`.
|
|
|
|
For header files, we have two relevant directories. `include/project_name`
|
|
contains all header files that are part of the public API for the library.
|
|
`src/_include` on the other hand contains header files that are only used
|
|
internally by the project. Here we once again have the same split where
|
|
`src/_include/project_name` contains internal header files native to the
|
|
project, while thirdparty header files can be placed either directly in
|
|
`src/_include` or in their own subdirectories.
|
|
|
|
Finally we have `test` and `example`. `test` contains unit tests, while
|
|
`example` contains source files that illustrate how to use the library in a
|
|
practical context.
|
|
|
|
This setup seems to be fairly standard, and it works perfectly for me. To power
|
|
a C project, we of course need some form of build system, so let's talk about
|
|
*the Makefile*.
|
|
|
|
## The Makefile
|
|
|
|
During my years of creating personal projects I started leaning more towards a
|
|
lightweight development style. For a while I was a big fan of CMake, but for my
|
|
projects it's way too complex. As a replacement, I opted for a hand-written
|
|
Makefile. While I'm not going to go into detail on the specifics of the
|
|
[Makefile](https://git.rustybever.be/Chewing_Bever/c-template), I will mention
|
|
its most predominant features.
|
|
|
|
First and foremost it supports compiling all required files and linking them
|
|
into either a static library or a native binary, depending on the project. It
|
|
allows all source files to include any header file from both `include` and
|
|
`src/_include`. Unit tests and example binaries are compiled separately and
|
|
linked with the static library. Unit tests are allowed to include any internal
|
|
header file for more precise testing where needed, whereas example binaries
|
|
only get access to the public API.
|
|
|
|
The Makefile properly utilizes the `CC`, `CFLAGS` and `LDFLAGS` variables,
|
|
allowing me to build release binaries and libraries simply by running `make
|
|
CFLAGS='-O3' LDFLAGS='-flto'`. Make also allows running compilation in parallel
|
|
using the `-j` flag, greatly speeding up compilation. A properly written
|
|
Makefile really does make life a lot easier.
|
|
|
|
It also solves a common issue with C compilation: header files. The usual
|
|
bog-standard Makefile only defines the C source file as a dependency for its
|
|
respective object file. Because to this, object files do not get recompiled
|
|
whenever a header file included by its source file is changed. This can result
|
|
in unexpected errors when linking. The Makefile solves this by setting the
|
|
`-MMD -MP` compiler flags. `-MMD` tells the compiler to generate a Makefile in
|
|
the build directory next to each source file's object file. These Makefiles
|
|
define all included header files as a dependency for its respective object
|
|
file. By importing these Makefiles into our main Makefile, our object files are
|
|
automatically recompiled whenever a relevant header file is changed.
|
|
|
|
The Makefile also contains some quality-of-life phony targets for stuff I use
|
|
regularly:
|
|
|
|
* `make lint` and `make fmt` use `clang-format` to lint and format the source
|
|
files
|
|
* `make check` runs `cppcheck` (and possibly other tools in the future) on the
|
|
source code, notifying me of obvious memory leaks or mistakes
|
|
* `make test` compiles all test binaries and runs the unit tests
|
|
* `make run` compiles and runs the main binary
|
|
* `make build-example` builds all examples
|
|
* `make bear` generates a `compile_commands.json` file using
|
|
[Bear](https://github.com/rizsotto/Bear) (the `clangd` LSP server requires
|
|
this to work properly)
|
|
* `make clean` removes all build artifacts
|
|
|
|
## Testing
|
|
|
|
My setup currently only supports unit tests, as I haven't really had the need
|
|
for anything more complex. For this, I use
|
|
[acutest](https://github.com/mity/acutest), a simple and easy to use
|
|
header-only testing framework that's perfect for my projects. It's fully
|
|
contained within a single header file that gets imported by all test files
|
|
under the `test` directory. By having the testing framework fully contained in
|
|
the project it also becomes very easy to run tests in a CI. If the CI
|
|
environment can compile the library it can also run the tests without any
|
|
additional dependencies required.
|
|
|
|
## Combining projects
|
|
|
|
My projects, specifically libraries, often start as part of a different project
|
|
(e.g. [lnm](https://git.rustybever.be/Chewing_Bever/lnm) used to be part of
|
|
[Lander](https://git.rustybever.be/Chewing_Bever/lander)). As the parent
|
|
project grows, some sections start to grow into their own, self-contained unit.
|
|
At this point, I take the time to properly decouple the codebases, moving the
|
|
new library into its own subdirectory. This subdirectory then gets the same
|
|
structure as described above, allowing the parent project to include it as a
|
|
static library.
|
|
|
|
This approach gives me a lot of flexibility when it comes to testing, as well
|
|
as giving me the freedom to separate subprojects into their own repositories as
|
|
desired. Each project functions exactly the same if it's a local subdirectory
|
|
or a Git submodule, allowing me to easily use my libraries in multiple projects
|
|
simply by including them as submodules.
|
|
|
|
## Outro
|
|
|
|
That was my C project setup in a nutshell. Maybe this post could be of use to
|
|
someone, giving them ideas on how to improve their existing setups.
|
|
|
|
As is standard with this blog, this post was rather technical. If you got to
|
|
this point, thank you very much for reading.
|
|
|
|
Jef
|