Back to C

I've recently been programming seriously in C, after around 10 years in higher level languages (Go, Python, C++, and others). I've been using C11, the latest standard, whereas previously I was working in C89.

I like programming in C. It's not an easy language to write fluently because it doens't provide many conveniences, it's full of traps, and I'd avoid it if I was writing something that needed to be safe, but I still find it fun.

This post describes my recent experience with C and the new standard, and my observations about how things have and haven't changed. It's not a critique of C, and some of the obvious problems with writing C (such as lack of bounds checking of arrays) aren't discussed.

The good

First the good things about C11 (relative to C89 — these things may have appeared in in-between standards).

The new standard integer types, int64_t, uint32_t, int8_t and so on, are a very welcome addition. Previously, typically one would have to generate a header file with platform-specific definitions with the same effect, but it's nice to have it in the standard library. That you need to write, for example, printf("a 64 bit number: %" PRIu64 "\n", (uint64_t)42); to print one out is pretty ugly though.

Similarly, it's nice to have a bool type built into the language.

Being able to specify struct field names, and array indices in literals and initializers helps readability (and, like in Go, allows one to extend structs with backwards-compatibility if code treats zero values carefully).

I find myself writing code like this quite often, which initializes a struct pointer's fields, including zeroing the unspecified ones:

*item = (layerItem_s){
    .props = LAYER_ITEM_PROP_ELEMENT_TRIANGLES | LAYER_ITEM_PROP_BLEND | LAYER_ITEM_PROP_TEX0,
    .vao_index = vao,
    .offset = buf_offset / sizeof(font_vertex_s),
    .n_elements = vx_count,
    .tex0 = texture,
};

Many functions in the standard library that set errno now have a "safe" _s variant that returns an errno_t rather than using a global (or thread-local) variable to report the errors. For example:

FILE *f;
errno_t err = fopen_s(&f, "data.txt", "r");
... code that checks err.

The lack of multiple return-values makes this a bit ugly, but this seems like a step forward to me, both because it makes error-checking a bit more obvious, but also because it avoids the problems with the global errno.

Having clang-format is really nice, for all the usual reasons that automated code-formatters are nice.

Finally, compilation speed with clang is tolerable. On my development machine, I can run a parallel make, and get a binary in around 2 seconds. Ideally it would be faster, but it's quite acceptable.

Things I miss

Having to write separate .h files, and in general, the lack of modules or packages or whatever else you might call them, is a chore. The Go model of packages possibly being multiple files, and having a way of specifying what's public and what's not works really well, and the bonus of getting explicit dependency information makes the build process more easily toolable. I don't want to have to write a Makefile to incrementally compile a library or binary. I don't claim that adding modules/packages to C is straightforward, but it's something I found a bit painful.

I missed something like C++'s RAII or go's defer. With explicit memory management, in many cases one wants to allocate at the start of a block and free at the end, and not have unruly code to handle the frees when you break out of the block or return from a function mid-block.

I also missed objects. Nearly all mainstream languages now have a way of specifying methods for objects. Being able to define methods (even if there's no generics, interfaces or inheritance), is a natural way of grouping functionality with the associated data, and the method names are not in any global or package namespace, so they can be shorter. I'd much rather read a function call that takes an item and an integer that looks like item.F(123) than how you'd write it using the convention of prefixing function names with the package that defines them: item_F(item, 123).

I'd really like a smarter printf, which I guess would require much smarter macros like in zig. Not only would it mean that for example you could write %d whatever the specific integer type, but also allow better error-checking, and perhaps even enable custom formatting code.

The awful

Concurrency and parallelism are still not properly in the language. I was extremely happy to see a threads module in the standard library (threads.h), but the I discovered it was an optional extension, and isn't available everywhere (including on the clang distribution I'm using). The obvious alternative choice on Linux is pthreads, but on Windows either you use the Windows API, or download a port of pthreads from sourceforge.net. What a mess.

Debug information is not standardized. Linux has a nice execinfo.h module that provides a simple way to get backtraces, and Windows has a quite complicated API that also requires separate debug symbol files. I just want debug information compiled into my debug build, and for there to be a standard API for parsing the stack.

Dependencies are awful. In my code, I've minimized my third-party dependencies, and only currently depend on OpenGL and SDL2. But still, I found it extremely painful to find the right libraries to link to, and OpenGL in particular has become user-hostile, requiring "glew" on Windows to use the latest versions. I still haven't managed to build a statically-linked binary on Windows using the clang compiler and SDL.

Conclusion

Overall, I found the newer features of C11 I used beneficial, but relatively superficial. I consider the lack of availability of threads.h to be the biggest disappointment, given the modern world with its multi-core CPUs.

Mostly, I found C to be as good as it ever was. Fun and efficient, but error-prone and with rough usability.

Paul Hankin Written by:

Programming since 1981, professionally since 2000. I’ve a PhD in programming language semantics but these days I prefer programming in Go, C, and Python. I’ve worked mostly on games and large-scale server software.

comments powered by Disqus