C is my first programming language I learned. It took me a very long time to make the decision to learn it among so many options. I thought it was the very best choice. Today? I partially agree with it, because C is perfect to learn programming. Let me remake this sentence: Perfect to learn programming, not just how to write C code, but programming. This was the main source of knowledge that powers me today to pick any language and just go write stuff.
Over time I came with many difficulties while writing code in it, so I started a library called aoclibs.h, a collection of libraries written from scratch, with the goal to aid me write the things I wanted to do. And most of my experience with C was developing it, and exploring the different areas of memory management, strings, slices, pattern matching and the whole macro cursed rabbit hole.
I love C for the fact of it being so simple, as weird as it may sound, easy to read, and barebones, at least on surface. But C sucks for everything else. I had to find better solutions to manage memory, because there was no easy way to manage them, without verifying that each conditional branch could leak memory, or deal with errors in dozens of different ways, because people came with clever ideas to do it, or even have to deal with the most basic stupid type issues.
A very interesting approach I employed in my aoclibs.h library, was to write C in a way that it’s fully compatible with C++ without extern "C". This lets me compile a valid C program, using the C++ compiler, and get a much better type system, that C could never provide. And in fact, just by following all C++ errors in that library, I was able to find many bugs.
Take the following example in C, which is also valid C++:
#include <stdbool.h>
#include <stdio.h>
void *foo(void) { return 0; }
int main(void) {
bool ok = foo();
if (ok == 0) {
printf("valid C\n");
}
return 0;
}Compiling this using the following: cc -Wpedantic -Wextra -Wall -o main main.c.
This compiles without emitting a single warning or error, but even in this case C++ doesn’t complain either. Note how we returned an integer from a function that it’s supposed to return a pointer. And when we collect the return value of foo, this is code is only valid, because we put the value in a boolean, which it’s primitive is _Bool (by this I mean that if ok was int the compiler complains), but false and true are actual macros that expand to 1 and 0, respectively. No way any language that came after C can do this atrocity.
I can’t really explain why C let’s you do this, but this is actually something that is exploited all over the place. And that is the NULL macro. This macro expands to the following: ((void*)0), it’s just zero… things like this makes me hate this language very deeply. The NULL macro casts zero to a pointer just for the sake of the compiler don’t throwing a warning, but this example I’ve shown doesn’t. You are free to return 0 instead of NULL, they are the same.
Another thing that annoys me a lot is forward declaration. In the beginning you hear everyone complain about it, but I was like: “no way this can be this bad”. It is. Once again let’s find how aoclibs.h is made:
The following is a trimmed output of the tree command in the source code.
├── build
├── build++
├── include
│ ├── base.h
│ ├── buddy.h
│ ├── calculus.h
│ ├── cfg.h
│ ├── colors.h
│ ├── cstr.h
│ ├── da.h
│ ├── file.h
│ ├── fork.h
│ ├── fquery.h
│ ├── hmap.h
│ ├── io.h
│ ├── libc.h
│ ├── map.h
│ ├── pp.h
│ ├── printfc.h
│ ├── rc.h
│ └── stack.h
├── libs
│ ├── crown.h
│ ├── debug.h
│ ├── heap_trace.h
│ ├── ini.h
│ ├── spinner.h
│ └── tunit.h
├── merge.c
├── src
│ ├── buddy.c
│ ├── calculus.c
│ ├── cstr.c
│ ├── file.c
│ ├── fork.c
│ ├── fquery.c
│ ├── hmap.c
│ ├── io.c
│ ├── map.c
│ ├── pp.c
│ └── stack.c
├── template.h
└── vendor
├── arena.h
└── rapidhash.h
Note how I do the classic splitting of header files and source files, but how do I came with a single header stb-style? The merge.c is responsible for doing that, no cmake, no make no meson. merge.c is itself compiled with cc @build or c++ build++. This will read template.h and replace all includes with files from include and libs, and at the very bottom append each file from src in it, producing a single header. You can infer some problems I have do deal with, in this approach. One of them is that template.h has to order all the libraries in a way that they can find all their dependencies, not only to mention they can be circular. Although, aoclibs.h works like a breeze, the step to make it is very painful, yet self-hosted without external dependencies, but the C compiler. The second problem is that I run my unit tests using the generated aoclibs.h, but this one is my fault for being lazy, and don’t make a better system for this. The problem is that I have to regenerate aoclibs.h and retry until the tests pass.
By the way, if you are curious on how my unit tests work. They are written along the source code, either in the
.cor.hfiles, and than transformed into a single executable by compiling them with-DTUNIT. All the tests run in the main process and execute all of them in a blink of an eye.
Yet another pain in the neck I have is making reusable code across different types. New languages often have builtin code generation like C++ templates, just to name one, however the only two ways to make code generic in C, is by using macros or void *. Which one is worse? One you increase you compile times unnecessarily, and the second you lose type safety. Because I care more about compile times, I made the following:
// hmap.h:36 (506b89e)
#define hmap_insert(map, slice, val) \
({ \
bool ok = false; \
typeof((map)->entries) e = (typeof((map)->entries))_hmap_insert( \
(_HashMap *)map, (slice), sizeof(*(map)->entries)); \
if (e) { \
e->value = (val); \
ok = true; \
} \
ok; \
})
AOCDEF _HashEntry *_hmap_insert(_HashMap *map, const Slice key, const size_t sizeof_entry);Note how I run the actual code in a function, and all the operations that would require me to do more advanced error prone tricks with offsets and the size of the struct, I did them in the macro level. This is terrible code, I’m not proud for writing this. Don’t copy me. At least this is not worse than what people did in here: google/cwisstable, by generating entire functions and structs with macros, and losing all of the debug information that could be useful in a debugger.
We can’t forget about naming in C. It only takes you to write a function called foo, and leave it visible for you to have very nice naming collisions, this way we don’t write foo, we write: mylibname_foo, if not: mylibname_modulename_foo. The modern languages let you do it, because they turn the name into absolute garbage in a step called mangling (taking your C ABI compatibility away in the process), so only them we can be happy and write foo. Personally, I don’t prefix aoclibs.h with aoc, because nobody uses it anyway, but me. But even by taking this assumption, I still have to bother a lot on how I name my functions, because as I say, I use the C++ compiler in my C source code, which can interfere with stdlib++.
What about managing memory? C doesn’t have defer, and if you want to force it in, you can with varying levels of nice interface. For the nicest you need C23 with GCC, but every other compiler doesn’t compile the code. The simplest more portable requires __attribute__(__cleanup__(fn)), and I tried doing something very fancy with it, and inject it in an experimental allocator interface, all you needed to do was Heap memory auto_free = heap_init(allocator, allocator_context);, where auto_free was #define auto_free __attribute__(__cleanup__(_heap_free)). The result? My library would be twice as much complicated to read and write, than if I don’t do them. I’m condemned to rely on arenas. In the Linux kernel source code, there resides a file called cleanup.h which also uses from this attribute, but also overcomplicates everything, behind dozens of macros, unlike mine with one macro.
Have I already mentioned, that one my practices in the codebase is to put null in all pointer that can return NULL, as a user-only clue, without clang’s bizarre _Nonnull and _Nullable keywords. To ensure that the pointers that don’t have null in it are always valid, I do put $assert_nonnull(variable_name) all over the place. That’s very nice, and helped me debug some things more easily, but it’s manual. There is no such reference feature, that guarantees the variable is always valid, and is not yet another footgun like C++ references.
1 Closing
I’m very confident to say, that the best first programming language to learn as a beginner is C, but is one of the last languages you should actually ship stuff in. I mean, it’s fine, but too error prone. I had to learn it the hard way. But it’s absolute worth it, to put Python aside, and learn programming first.
So what is the best language to ship stuff in? What about Rust? You may ask. For me Rust is the same experience of writing code like C, with some annoyances taken away, and new ones added to it. For me, the borrowing system, ownership and lifetimes are rather simple to understand and use, and that’s the main factor that brings safety to the language, however the entire set of safe and unsafe codeblocks for me, it’s just a promise system from the users, rather than real enforcement of safety by the compiler. It’s totally valid to take a gigantic function that is pure unsafe, and tell the function is safe, it’s just a promise. And among promises upon promises the compiler may render itself not helpful when you most need it.
When I write C, and I get a nasty bug, I always think with myself: “it would be so much nicer if I’ve had written this in Rust, and the compiler tell me what’s wrong”, until it can’t, and then I’m cooked again.
Another worth mention is one my most recent projects I started: odd. This is an attempt to make a better GNU ed, for actual usable CLI-based text edits. I started it in Odin, so I could learn more about the language. However, it didn’t took me too long to find some horrible bugs in the code, and having no idea why, until I found out that was a classic bug I always had in C where I append the same pointer (but tricked, since the content of the pointer was always different) in the dynamic array, but it was so much more harder to debug, because I wasn’t used to Odin, so I couldn’t figure it out sooner, based on my old experiences. For me Odin is the most closest to C in usage, but addresses all my pain points in C, perhaps I was wrong to rewrite odd in C.
Anyways, hope you enjoyed. See ya.
This post is licensed under CC BY 4.0 by the author.