Modern C sucks and you should definitely NOT embrase it
I’ve recently stumbled upon an article about a “modern” C development environment. It caught my attention; sounds like a good read and potentially a way to learn something new and refresh the good old rusty C skills, so I thought. Unfortunately, it was quite to the contrary, leaving a bitter taste at the end. There’s a lot of stuff I disagree with and frankly some that should be killed with a shovel and buried deep in the ground before it spreads like a disease and make C development even more miserable than it already is.
TLDR
Thumbs up to:
- Docker
- clang-tidy/clang-format
- CI
- TDD
Definitive thumbs down to
The good things
Docker based development environment
It starts quite good. The author mentions the development environment with the
toolchain embedded in the docker container and a set of supporting tools like
clang-tidy
, clang-format
, gcov
etc and that’s a great suggestion! I’m all for
it! It’s a perfect way to have a unified development environment which is easy
to share between developers (and maybe even the CI). Something like
gitpod - which I plan to try and write about as well.
Additionally, docker allows you to test your code on different platforms
through qemu
so, that’s another argument to use it. So, to summarise thanks to docker you get:
- unified development environments
- tooling guaranteeing consistent formatting and code quality
- CI environment
- multi platform builds
Github actions
Yep. 100% on board with that. Whenever possible there should be a CI system building your code and running your tests. It may feel like a chore initially but trust me, you’ll thank yourself later.
The bad
Unfortunately it only gets downhill from now on.
Rust as a dependency to run clang tools
Soon after this great start, we’re getting into muddy waters. The general problem I have with this article is that the author is quite frivolous with the dependencies as far as your setup goes. He’s trying to advertise his own helper solutions to run clang tools and again, nothing wrong with that but… these are written in rust.
So now, we need rust… to run helpers… to lint/format C.
This is a bit disappointing. Especially considering that usually things like that can be handled with a simple shell script which saves you the trouble of having rust and cargo in your docker image. Not a great deal but honestly, feels a bit wrong.
Ceedling
My first reaction was… what is it? Quoting after Ceedling’s own documentation:
Ceedling
is a “test build manager”…
This statement on its own is very confusing. Is it a build system, package manager or something else? From a very superficial glance it seems like it’s targeted mostly to embedded environments and encourages the developers to follow TDD, allowing for seamless test execution either on x86 or simulators/emulators. To do that, it has to be a build system and dependency manager.
The problem is that it’s not good at doing that at all. It’s too limited as a build system (just looking at the examples it seems like the notion of a build target is very vague and basically your project is your target), doesn’t allow for proper dependency management (like package definitions or integration of 3rd party libraries) and… it’s written in ruby.
So to summarise, “modern C” setup so far, requires you to install ruby and rust. But wait, it
gets even better! The author later on suggests to use invoke as a
replacement for Makefiles which is written in python :D. Honestly, I have
nothing against ruby
, rust
, python
or even invoke
in particular but this is
way too over the top. I’m not sure if I should continue reading the rest of the
article; slightly afraid I’ll need golang
, JavaScript
or… C++
just to write
C
. But back to Ceedling
. It doesn’t seem to be production ready at the moment
and I had some difficulties running it on my Arch Linux machine:
|
|
Psych
- which is a YAML serialiser is complaining about something in
project.yml
- generated by Ceedling
itself :D. I’ve tried with ruby-3.3 via docker
and it had some problems as well:
|
|
This actually proves my point. As a C
developer I have no wish to deal with
ruby
problems! I was very persistent though and managed to get it working with
ruby-3.0 docker image. I’ve generated the two example projects blinky
and
temp_sensor
with the intention to inspect the code.
My first impression (and I’m happy to be corrected on this one) that it’s a single
target system reinforced. It takes the list of files you give it and links with a list of
libraries you tell it to. The output is a single binary in whatever shape or
form you need it, bin
file, hex
file or elf
binary. It’s unable to build
the dependency graph to be able to build the dependencies for you and for the
most part relies on dependencies provided in pre-compiled form.
Additionally, it’s trying to sell you all other libraries/solutions from throwtheswitch.org which are honestly of dubious quality and encourage bad practices which takes me to cexception.
cexception
Before I even begin…
Right, with that out of the way, why? Contrary to what
throwtheswitch.org guys are trying to teach you,
you don’t want exceptions in C and especially not implemented using
setjmp
/longjmp
unless you want to be get yourself shot in the foot in the
least expected way when dealing with resource leaks! If you find yourself
needing exceptions in your project then C is probably not the right choice of a
technology to begin with! CException
is basically a set of macros
instrumenting setjmp
/longjmp
under the hood.
setjmp
/lonjmp
lead to unpredictable control flow and in the
long run will make your code unmaintainable. Implementing
exception which facilitate setjmp
/longjmp
is a bad idea
because it will inevitably lead to misuse. We all know how
exceptions work and what to expect from them in languages like C++. The
problem is that in C, you can’t transfer these expectations
directly. Consider the following simple snippet:
|
|
This will produce the expected behaviour. But what if I just call b
directly
in main
?
|
|
Most likely this will lead to a SEGFAULT since env
is uninitialised. In C++
this is perfectly valid though. Sure it will end with and an uncaught
exception and a call to std::terminate
- still though, completely valid and
well defined behaviour.
The above was a very contrived and simple example. Imagine the problems you
may face once your system gets more complex! Not to mention problems deriving
from implementation bugs of CException
itself.
C has no exceptions! Any attempts to emulate them will lead to convoluted problems. So, simply don’t do it. Code in any language should be idiomatic!
Alternatives?
Use tested well established technologies with a good community and support.
- Toolchain: gcc/clang - or whatever is provided by your vendor
- Build system: meson/cmake with make or ninja - None of that
Ceedling
nonsense - Dev environment: Docker/gitpod
- Formatting/LSP: clangd/clang-format/clang-tidy
- Testing framework: gtest+gmock/catch2 - nothing wrong with C++ testing framework - it’s even good. C++ compiler is more strict so, compiling C with C++ compiler on its own might be beneficial.