c-project-setup: added post
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
parent
0897a275ee
commit
c71d9f723b
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue