Please see the disclaimer.

Assumed Audience: Programmers, hackers, software engineers and technology executives. Discuss on Hacker News.

Epistemic Status: Confident and maybe a little cocky.

tl;dr: C is the only existing language I like to actually write.

Introduction

The NSA, bad as they are, released some good advice recently.

It caused a major brouhaha.

Which is stupid because it’s common sense advice at this point, and without a good reason, this advice should be followed by default.

I especially don’t buy the arguments of Bjarne Stroustrup that C++ is not that bad.

In fact, I think the complexity of C++ makes it worse than C.

I hate C++.

Jimmy Hartzell is right that:

For memory safety, “much of” is not really good enough, and “could be made” is practically worthless. Fundamentally, the point is that memory safety in C++ is a project being actively worked on, and close to existing. Meanwhile, Rust (and Swift, C#, Java, and others) already implements memory safety.

As much as I hate the Rust Evangelism Strike Force, they are right about this, and Rust is the current best option for a C or C++ programmer.

So…my current project, an ostensibly professional project, is in C.

Am I a hypocrite?

Yes, actually, but please hear me out for the rest of the post. I have reasons, I promise.

Documentation

The first reason is that I document everything about my assumptions.

Rust is great in many ways, and it is great at having code document itself.

For example, with explicit pointer types, it’s easy to see that they make it easier to see how to use function parameters properly.

C just has value and pointer types. A pointer could be dynamically allocated or point to something in an ancestor’s stack. Or something else entirely.

Rust has better type safety. You give C a void*, and it just shrugs and accepts it as a pointer to anything.

Rust is better in those ways. Objectively.

I make up for it in various ways:

  • I document all of my assumptions that I can think of, usually with code (in asserts).
  • I document every function, its parameters and their uses.
  • I document every struct, every field, every enum.
  • I write design documents and development documents.

This means that I get many of the advantages of Rust and also have good documentation, which is delightful for my users later.

That’s not to say that Rust is not better here; I just like that this forces me to write documentation.

I Work Alone

Second, I work alone on code that is entirely in my head.

Rust is great for teams because it removes many things that make working on team code dangerous. The items mentioned above are some.

I work alone, however, because I like to keep my code in my head and working with people means parts of the code are only in their head.

This also means that the bigger the language is, the less space I have in my head for the code.

Rust is too big for my small brain, unfortunately.

Rust Isn’t Fun for Me

This brings me to my next reason: I don’t enjoy Rust.

Now, before the RESF gets on my case, let me say that this is purely a personal preference! I am not saying that Rust is bad.

I’ve written code in so many languages:

  • C
  • C++
  • Rust
  • Haskell
  • Go
  • Python
  • PHP
  • Ruby
  • Zig
  • Bash
  • POSIX sh
  • bc
  • dc
  • JavaScript
  • Clojure
  • Java
  • C#
  • Various assembly languages
  • GLSL
  • Common Lisp
  • Racket
  • Julia
  • Tcl
  • Vim script
  • Lua

I have also studied far more, such as:

  • ML
  • OCaml
  • HAL/S
  • Pascal
  • Objective-C
  • MATLAB
  • Modula-2
  • Coq
  • Isabelle/HOL
  • Ada/SPARK

and many more.

When it comes to the enjoyment of programming, I hate all of them. With a passion.

Except for C.

😲

That’s right: C is the only existing language I like to actually write.

I’m not kidding either; I am far more productive writing in C than anything else by orders of magnitude. My mind just works with C, and I don’t know why.

That matters more with me than most programmers, too. From my experience, I am a temperamental programmer; if my environment is not correct, I can’t program effectively.

I can’t program effectively if I’m tired.

I can’t program effectively if I’m listening to slow music. (True story.)

I can’t program effectively if I’m overwhelmed by a task.

I can’t program effectively if I don’t have the entire software in my head.

I can’t program effectively if I think my wife is mad at me.

I’m usually overthinking things when I think that. My wife is lovely and patient.

And finally, I can’t program effectively in a language I do not like.

I’ve turned down a job offer because it was in C++, and I hate C++.

So I don’t use Rust or another memory-safe language because chances are, I’ve tried it, and I just can’t work effectively in it.

I know this is a personal problem and not a Rust problem. But boy, am I good at C.

Not perfect, though.

And I love it.

I love it so much that I actually enjoy hunting down memory bugs and memory leaks!

I know, I’m a freak.

But because I enjoy it, I do it.

Discipline

This brings me to my next reason: I have the discipline to write C at a high level.

Discipline takes many forms in this case:

  • I use only unsigned integers to avoid undefined behavior. I even simulate signed 2’s complement with unsigned integers.
  • I use a macro to make Clang complain if I don’t use the return value of a function I wrote.
  • I fuzz. I fuzz a lot. I use fuzzers to generate my test suite.
  • I run Clang and GCC with -Wall, -pedantic, and -Werror. This means I eliminate all warnings at every stage of development.
  • I even run Clang with -Weverything, except for -Wpadded because I don’t care about padding in my structs (I carefully choose their layout) and except for -Wc++98-compat because I don’t care about C++ 98 compatibility, which interferes with C11 stdalign.h.
  • I run sanitizers on my test suite and eliminate everything. Yes, everything, even false positives.
  • I run Valgrind on my test suite and eliminate everything.
  • I run Helgrind on my test suite and eliminate everything, even false positives.
  • I run static analyzers and eliminate everything, even false positives.

You have no idea how good it feels to run Valgrind, or Clang, or ASan, or TSan, again and again, smashing bugs again and again and finally see nothing.

Ah, sweet bliss and satisfaction.

I…may have just a touch of OCD when programming.

Experience

That leads to the next reason: experience.

I have spent so much time with C doing that bug smashing, which means I have spent more time with C than any other language. I know C far better than anything else that might compete, and it isn’t even close.

That time would be hard to make up. If I started with Rust today, I believe it would take me about a decade to get as good with Rust as I am with C.

That’s a lot of lost time.

Compilation Times

Lost time is central to the next reason: compilation times.

Now, I know Rust is getting a lot better. This is great!

But it’s still slow. On my 16-core machine, I can do a clean build of my current project in the same amount of time it can take Rust to build one file. This includes the test suite, which uses C for many separate test cases.

Another aspect is that the C file include model, as bad as it may be, is still embarrassingly parallel. Rust is good, but it isn’t as good.

This is one reason I’ve designed my language to need as few dependencies between files as possible, to recover some of that embarrassingly parallel sweetness.

Custom Memory Safety

The next reason is that during my time with C, I have developed a custom software stack designed around memory safety.

  • I have special array types that I use instead of straight pointers, and those arrays store their bounds.
  • I have macros to index those arrays with a bounds check.
  • I have macros to calculate pointers from those arrays with a bounds check.
  • I have a stack allocator that allows me to pretend I have alloca() on every platform.
  • The stack allocator lets me base every allocation in a stack frame.
  • The stack allocator knows about destructors, so it will call destructors when freeing memory.
  • The stack allocator will free everything allocated in a scope if I call one particular function.
  • The stack allocator will free everything allocated in a function if I call one particular function.
  • I use structured concurrency, which means that if I pass data to a new thread, I ensure that thread does not outlive the data passed to it.

Simply put: every piece of data can trace its ancestry to a particular spot on the stack, and data is never given to another object if that object’s lifetime exceeds that of the data.

This means RAII, which means lifetimes and ownership, which means borrowing, which means I’ve reinvented the best parts of Rust in portable C11.

Basically. Having a compiler enforce stuff statically is still better.

All of this means that items are always deallocated in the same function they were allocated in or as part of the same parent object that owns them.

This vastly reduces my double frees and use-after-frees. And because all of my code does this, without exception, I might just eliminate them with enough testing.

And the bounds checks obviously reduce the buffer overflows.

Of course, Jimmy Hartzell’s words still apply: anything less than perfect is not good enough.

In other words, I am not using C; I am actually using the partially memory-safe Gavin D. Howard dialect of C.

I Will Rewrite It

The final reason is the only one that justifies my decision: my code will be rewritten in a memory-safe language.

No, it won’t be Rust. It’s my own language. It’s called Yao.

It’s currently under development as part of my current project, which is why I’m not using it now.

As part of its bootstrap process, it will always be able to transpile to C. That’s why I’m starting in C.

In fact, part of the reason I built custom memory safety stuff is so that Yao could transpile to C and still be memory-safe.

This is the only reason I could justify that decision. All of the other reasons above are excuses. This is the real reason.

That doesn’t mean that there are not other reasons to use C. I used it before, to build a bc in C, but I did that because bc is needed to bootstrap a Linux system, when only a C compiler and basic POSIX utilities are available since bc is, itself, a basic POSIX utility. That justified the decision to use C for bc.

But nothing, and I mean nothing, could justify the decision to use C for software that nobody needs unless there was a plan to get memory safety soon.

And I do mean soon. I’m building a business around my current project, and as part of that business, and because I believe I should, I will accept some liability for that project for paying customers.

You can bet that I do not want to accept liability for a project written in C. It will be rewritten as soon as possible.

The other advantage of doing it as soon as possible is that there’s less to rewrite.

Conclusion

So those are my reasons.

Of course, you may disagree with my decision, and that’s a valid opinion. It’s a good opinion.

But I can tell you this: I would not write this software at all if I didn’t enjoy writing it. And I only enjoy C.

So for me personally, using C is inevitable.