Contents

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:

1
2
3
4
5
6
7
8
9
/usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:432:in `visit_Psych_Nodes_Alias': Unknown alias: common_defines (Psych::BadAlias)                                                         
        from /usr/lib/ruby/3.0.0/psych/visitors/visitor.rb:30:in `visit'                                                                                                                
        from /usr/lib/ruby/3.0.0/psych/visitors/visitor.rb:6:in `accept'                                                                                                                
        from /usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:35:in `accept'                                                                                                               
        from /usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:340:in `block in register_empty'                                                                                             
        from /usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:340:in `each'                                                                                                                
        from /usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:340:in `register_empty'                                                                                                      
        from /usr/lib/ruby/3.0.0/psych/visitors/to_ruby.rb:148:in `visit_Psych_Nodes_Sequence'
...

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:

1
2
3
4
5
6
7
8
ceedling@f13a1ec86a0a:/usr/src$ ceedling help                                                                                                                                           
/usr/local/bundle/gems/ceedling-0.31.1/bin/ceedling:12:in `<top (required)>': undefined method `exists?' for class File (NoMethodError)                                                 
                                                                                                                                                                                        
  project_found = File.exists?(main_filepath)                                                                                                                                           
                      ^^^^^^^^                                                                                                                                                          
Did you mean?  exist?                                                                                                                                                                   
        from /usr/local/bundle/bin/ceedling:25:in `load'                                                                                                                                
        from /usr/local/bundle/bin/ceedling:25:in `<main>'

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…

Warning
ABSOLUTELY NEVER, UNDER ANY CIRCUMSTANCES, USE CEXCEPTION!!!

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
jmp_buf env;

void b() {
  printf("b begin\n");
  longjmp(env, 1);
  printf("b end\n");
}

void a() {
  if (!setjmp(env)) {
    printf("a try block\n");
    b();
    printf("a try block end\n");
  } else {
    printf("a catch block\n");
  }
}

int main() {
  a();
  return 0;
}

This will produce the expected behaviour. But what if I just call b directly in main?

1
2
3
4
int main() {
  b();
  return 0;
}

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.