diff --git a/config.toml b/config.toml index 43b4704..8d642f4 100644 --- a/config.toml +++ b/config.toml @@ -8,7 +8,7 @@ pygmentsUseClasses = true [params] description = "The Rusty Bever" - copyright = "Copyright © 2023 Jef Roosens" + copyright = "Copyright © 2024 Jef Roosens" dark = "auto" highlight = true diff --git a/content/posts/c-project-setup/index.md b/content/posts/c-project-setup/index.md new file mode 100644 index 0000000..b32e5a8 --- /dev/null +++ b/content/posts/c-project-setup/index.md @@ -0,0 +1,142 @@ +--- +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