c-project-setup: added post
ci/woodpecker/push/woodpecker Pipeline was successful Details

main
Jef Roosens 2024-03-28 17:40:07 +01:00
parent 0897a275ee
commit c71d9f723b
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
2 changed files with 143 additions and 1 deletions

View File

@ -8,7 +8,7 @@ pygmentsUseClasses = true
[params] [params]
description = "The Rusty Bever" description = "The Rusty Bever"
copyright = "Copyright © 2023 Jef Roosens" copyright = "Copyright © 2024 Jef Roosens"
dark = "auto" dark = "auto"
highlight = true highlight = true

View File

@ -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