I fell into that trap and now with an account I also can't read it. I don't recommend creating one, you'll see the same annoying message but with "upgrade for $X/month to read it" message.
The front cover of the “C Programming Language” book as I recall it from memory
Why on earth would someone would pick C to start a new project in 2020? Surely there is a newer language with more shiny features that’s better right? Well I can’t speak for other people but I’ll tell you my reasons.
First of all let me preface this by saying that of course this is a biased opinion and the language I pick for something depends on the context it’s going to be used in. For example; I doubt I’ll ever be reaching for C when writing a web service simply because the ecosystem around that domain isn’t great and I’m not itching to write my http framework at this time.
But for games, more specifically cross-platform games C is a clear winner for me because it provides me with exactly the things I’m looking for which is reliability, simplicity and performance.
Reliability
This is the most important and only real non-negotiable requirement for me; the reliability and portability of the toolchain and the access it provides to system and device interfaces.
Languages, especially dynamic ones and their runtime environments come and go like fashion. When your target platform suddenly gets deprecated from the runtime you’re relying on, well then you’re just fucked and out of luck.
I’ve been in this situation before with multiple languages and I don’t intent to repeat history here so I need something stable, something will have a reliable toolchain available on all the platforms I could ever possibly want in perpetuity without having to worry about compiler bugs as I’m more than proficient in the art of introducing my own bugs.
The ecosystem around C provides all of this. Even for the most obscure platform you can imagine there will be solid stable toolchain with a good set of native libraries that provide access to system and device interfaces for said platform.
Performance
While I will happily write software in slower languages without that much complaint, performance is fairly high up on the list when it comes to writing games.
Avoiding cache misses is key to that, so having any kind of dynamic language with an interpreter is hopeless. Even in the best case scenario that the platform of choice provides you with one of the magical JIT compilers available today, they’re still magical black boxes which makes it difficult to reason about performance characteristics object boxing/unboxing and cache locality.
Same goes for stop the world garbage collection, again there are some fairly impressive implementations out there; Go’s garbage collector comes to mind but it’s still a garbage collector and it will still lead to frame drops. If you have to use object pools to tip-toe around garbage collector killing the frame-rate then what’s the point of having one in the first place.
Even for a low fidelity game filled with chunky pixelated goodness having more cycles gives you a lot of room to experiment and still have plenty of room to add copious amounts of extra juice.
But It’s not just about running fast, battery life matters as-well. Fewer cycles means less battery consumption which is a major win if you ask me. As long as I can help it, I’ll do my best to avoid draining a user’s battery in five minutes.
Lastly, when it comes to the web and mobile the executable size also has to be considered. While it’s not a deal breaker it’s a nice bonus that if you work on it a little bit C executables built with WebAssembly essentially have zero bytes of overhead by default. It might seem like a minor thing but it’s the difference between loading instantly and taking 3–5 minutes to load on a connection.
Simplicity
While it’s not a critical requirement, a nice thing about C is that it is an extremely simple language which is a nice welcome break from working in monolithic languages that get jam packed with new features every time someone on the design team or committee learns about a new feature in another language causing a p...
It loads for me but the asynchronous loading makes it feel really slow. The page loads but it doesn't show the body until 4 seconds have passed. There is a 34KB image and a short text. It's kind of absurd how the asynchronous loading takes 3 seconds for a cached image.
> Why on earth would someone would pick C to start a new project in 2020?
Please ask yourself this before writing any code (For a cross-platform game).
By just reading this, I thought to myself if I were to write a cross-platform game in 2020, Is C necessary to accomplish this? Perhaps anything C can do, surely Rust can do it safer and arguably better.
The biggest advantage C has over Rust is the sheer breadth of platforms. As a developer, I would always prefer Rust over C, if possible, but sometimes C is more realistic.
In most cases, C would only be more practical on obscure or outdated hardware, though.
Pretty easy to integrate C libs as unsafe calls with rust's FFI. You're definitely in for an uphill battle if you have to manage a bunch of decade+ old makefiles with hundreds of special flags for each platform under the Cargo ecosystem, though.
I fear that we are slowly moving towards a situation where our underlying technology stack is written in languages that a diminishing number of people have seniority in.
The GitHub counters are not always accurate. 97.3% (if going by C (imp+header) and Assembly) of the kernel is written in C, with the rest being written in Assembly. Everything else is config files, scripts, tests, or docs. GitHub reports C++ and Objective-C; those are just .h header files.
Update: Got to my system so I could actually look at available files accurately:
C Implementation (.c): 28269
C Header (.h): 20250
Asm (.S): 1323
Edits: Update section and removing "100% in C" and "5 sources files of assembly"
I wasn't at my system when I wrote that and just checked (5 is way off the mark there). Should have followed my own advice and not used GitHub's counters :P
The counter reports 1.2%, but if you click on that to show Assembly files it only reported 5 code results (it's not actually showing all Assembly, so you then have to click on "Unix Assembly" on the side).
Would be nice to get a survey of current Linux kernel maintainer demographics (age) and see where we're at. Can be good data for a call to arms and potential company backing via charitable funds.
While C is indeed a winner for obscure platforms, many language gets you perfectly covered for a "cross-platform" game.
> Performance
You can get equally good performance in Rust and C++.
> Simplicity
As he says, simplicity does not mean easy nor intuitive. Then it is a matter of opinion why simplicity should be a good thing. Personally, I prefer a complex but easier to use language than a simple but difficult to use language.
People need to stop acting like Rust is flawless. There are real issues that make it not an ideal choice in some situations (notably embedded). Issues include:
1. Rust is very slow to compile. This is bad when rapid iteration is important.
2. Rust is a complex language. It wants to replace C++, not C, and gladly adopts features™ and the complexity that comes with that.
> Rust is a complex language. It wants to replace C++, not C, and gladly adopts features™ and the complexity that comes with that.
This is a good point. I wonder if the trend of saying C/C++ is to blame; that is, people see C, think C/C++, and then are incapable of realizing that C is, in fact, its own language, and not merely "C++ Jr." or C++ with some features removed.
There's a hole in the modern language world, from what I can see: We lack a simple, mid-level language which incorporates modern knowledge about language design, where by mid-level I mean that all resources (other than stack, perhaps) are managed by hand but it's reasonably simple to write cross-platform code.
I've spent the last 2 years writing an age of empires 2 clone in C:
github.com/glouw/openempires
and my time spent with C has really just been manually implementing min heaps, circular buffers, ad hoc vectors, and a tier 1 dummy garbage collector.
All this stuff is readily available in C++, and I could have saved hundreds of hours by just using C++.
But, to one benefit, since I did all this work myself, I understand the machine better, and my compilation times are orders of magnitude faster than what I would have had if I chose C++. Additionly, since so few calls are even being made to the C standard library, the majority of the code exists in text, and easily inlines across all translations units with link time optimization. With C++, as except for templated library code, the majority of link time optimizations go to waste as calls to the standard library are binary linked.
That does give some food for thought. Of all the new language features, the elements I really need —functors, asynchronous syntactical sugar, and static null analysis— don't require much of the bloat found in many modern languages.
I assume that since he's writing a game, he's using some kind of library/engine/framework, but doesn't specify which one. This would affect his arguments for reliability (is the framework available on all these platforms?) and simplicity (how simple is the framework?).
I think since none of his reasons include I am doing system programming
I will debunk
Reliability: C is not the only language with a reliable ecosystem, and what should matter most is the reliability of the program, not the tools made to create the program, so in this regard, I think Rust, OCaml, C#, Java are all as reliable, and I would argue it is easier to write more predictable or reliable programs in them
Performance: Yes sure C wins almost every performance benchmark, but Rust is very Fast and Ocaml is also very fast so unless you are working on system programming stuff, I think you have better options that are more reliable and predictable
Simplicity: Here I would like to contrast simplicity of the tool vs simplicity of the product, complexity does not go away, complexity is abstracted, simple tool = complex product or process, complex tools on the other hand may allow you to create simpler tools and processes, if you use simple tools you will transfer complexity to your products, which I don't think is necessarily a good thing
Rust will frequently build code that will run in a comparable number of cycles, but can often run even faster as soon as you introduce concurrency. Safety isn't just some crutch to lean on instead of simply being really, really careful in C. Concurrent apps need a lot of guarantees for memory safety, and it takes a lot of mental energy to figure out how safe you really are. C devs often go overboard and do more than is necessary as a result.
"But It’s not just about running fast, battery life matters as-well. Fewer cycles means less battery consumption which is a major win if you ask me. As long as I can help it, I’ll do my best to avoid draining a user’s battery in five minutes."
I was thinking about languages in terms of ecology. I'm a Python fan and while its lack of performance never bit me in the real world, how many Joules would be saved if my apps were written in another language ? I'd love to see a study/comparison based on real apps (not algos implementations).
Who cares about fashion? In 15 years Ruby, Python, JavaScript, Perl, TCL (particularly for building simplistic but effective GUIs), and PHP will all not only be usable, but actively used.
He's talking about languages coming on going. "Languages, especially dynamic ones and their runtime environments come and go like fashion. When your target platform suddenly gets deprecated from the runtime you’re relying on, well then you’re just fucked and out of luck."
You'll build to almost the same number of raw CPU cycles executed for the code, but probably build a better-performing game in a language with safer/simpler concurrency/parallelism. Ever since futures (concurrency) has stabilised, rust's my first choice on this. Pretty much all of his points not related to the sheer age of C apply; you can even get the 0-byte-overhead webassembly if you go no-std, though I seldom forego something so useful that's smaller than the loading-screen background.
> For example; I doubt I’ll ever be reaching for C when writing a web service simply because the ecosystem around that domain isn’t great and I’m not itching to write my http framework at this time.
C has everything, including several top quality http frameworks. I use https://github.com/corvusoft/restbed and find it more reliable, more performant, and "better" in all respects than the php, js, node, and whatever frameworks I've been forced to learn.
I have programmed in C for a long time (for embedded before everything became ARM+Linux), and unless you have technical reasons to use C for a new project, I would skip it. I like tinkering with the low-levelness of C, but what applications really need that these days unless you're working with hardware/os level stuff?
There's also so much FUD about performance, but reality is that without numbers, you'll never know. Certainly for game development, unless you're working on a cutting-edge game engine, there's little reason to focus on performance first, and even then - most popular modern game engines offer interpreted paths for game logic.
Fear of garbage collectors is also bullshit in most cases. Many don't realise a simple call to free or malloc (or new/delete in C++) can result in just as much unpredictable hick-ups, which is why many games engines use specially tuned memory managers, some even specifically including garbage collectors (see the Unreal Engine for example).
Certainly in game development, flexibility to tune game mechanics is far more important to make a good game than performance, and creating such flexibility in a C application is a lot more complex/more work than in a higher-level language, and you'll end up building very high walls you really don't want to tear down and start over with.
The thing I find interesting about C is that everything and anything done in other languages can be done in C; after all, how does that language implement feature X? In some low level manner, and that is where C shines. C is not just the language, it is the entire ecosystem of all available source code written in C - compilers, browsers, you name it, it is there. Working in C is as much research and integration as one wants, with the results being any flavor of programming style you want - with all the foundation bits under your complete control. Plus the community of pure C developers is people that have been working in it for decades, with more practical programming wisdom than you'll find in shops with teams of high level developers releasing modern applications. The C teams release them too, but theirs is exponentially higher performance in exponentially smaller foot prints. Lots of software shops appear to be more into empire creation than quality software, but that's a result of decades of software industry propaganda too.
> The thing I find interesting about C is that everything and anything done in other languages can be done in C
With emphasis on "can". Not that you should. This is exactly what I mean, in many cases it's interesting for an intellectual exercise, but I would avoid it for new projects unless I really can't.
"Reliability" - in a language with manual resource management and unsafe parallelism model? Really?
BTW never heard of modern games written in C. Mainstream game engines (UE, Godot, Unity...) are written in C++ and C# and cover all major platforms and consoles.
"Performance" - Rust is probably more performant when it comes to code, and definitely more performant when it comes to development time.
"Simplicity" - primitivism is a closer definition.
Rust is not close to hardware as C is, so if need to do anything with hardware Rust still relies on majority of the code written in C. Performance wise today C is still faster than Rust in general.
Rust is still a decade or more away from anything serious like C. Majority of the functional Rust libraries today depend on underlying C libraries.
Rust may try to replace C++ but replacing C is far away dream.
Let's wait when Mozilla (origin and major supporter for Rust language) can release Firefox in Rust, until than it's still a popular experiment in systems programming area.
Rust does not run any critical component like Linux Kernel today, when it will be able to do it than it can be considered as real systems programming language challenger to C. Right now most of it's safety guarantees are not really battle tested like C.
"Rust does not run any critical component like Linux Kernel..." - and the only reason why it is not happening (provided that C and Rust have perfect interoperability, more and more C/C++ developers get familiar with Rust) is the fact that Linus Torvalds convinced by voices from Ethernet socket not to use anything but C. I can't wait for him to retire so that Linux kernel devs could pick up more modern toolset
> What do you think of the projects currently underway to develop OS kernels in languages like Rust (touted for having built-in safeties that C does not)?
> That's not a new phenomenon at all. We've had the system people who used Modula-2 or Ada, and I have to say Rust looks a lot better than either of those two disasters.
> I'm not convinced about Rust for an OS kernel (there's a lot more to system programming than the kernel, though), but at the same time there is no question that C has a lot of limitations.
As much as I love Rust: As long as Rust doesn't support all platforms which the Linux kernel supports, it really is a no-go for kernel code in general.
"in general" is a bit strong; the maintainers have said:
> I spoke with Greg Kroah-Hartman, and he said he'd be willing to accept a framework in the kernel for writing drivers in Rust, as long as 1) for now it wasn't enabled by default (even if you did "make allyesconfig") so that people don't need Rust to build the kernel, and 2) it shows real benefits beyond writing C, such as safe wrappers for kernel APIs.
Rust has a thriving embedded ecosystem[1]. You have access to an ever growing number of HALs[2]. There is an embedded operating system written entirely in Rust called Tock [3], and it can run things like OpenSK, an open source security key application. [4]
In reality I use anything that gets the job done faster. That usually means prototyping in C++,lisp, python, swift, java, perl, lua.
After I have the prototype I code it automatically with C, or minimal C++ and my own DSLs, done in Lisp controlling C, C++ code generation.
I have reinvented the wheel a lot. Someone told me you are not going to be smarter than a compiler, but having read several of them, they are so dumb, and they have to be because they are generic.
E.G In a project I was using std::vector and predicted the expected performance of the whole program by O notation and "on the park" calculation needs as X.
When I completed the program, it was X/1000. Fair enough I made some stupid mistake...in the end I got X/100 performance and could not optimize farther.
I got so frustrated so I started replacing components to see if numbers improved. Replacing std::vector by a c vector made the program X/1.4!!
So I started studying std::vector implementation and it was a template of a template of a template... I could not wrap my head around such a mess.
My implementation was done in lisp with proper macros and was way simpler and efficient and generated c code, that was so easy (although tedious) to understand.
Reading std::vector or most parts of the C++ standard library is the path of insanity. It puts you in touch with some fairly arcane parts of C++, and on top of that you have the allocator class, which std::vector inherits from rather than embeds because if it were embedded, it couldn’t be zero bytes (otherwise every object wouldn’t have a different address).
By comparison, glibc isn’t so bad to read. Same with the standard library for Python, Go, Rust, etc… I have never had such a problem reading library source code as I have with the C++ standard library.
If a std::vector is causing performance problems there is some major disconnect between how it is being used and how it is meant to be used.
Someone else noted that debug more will stifle performance, which is true and the most likely culprit.
Other problems can include constant resizing and memory allocation and copying, but when something is 1/100th the speed it should be, that screams debug mode being left on, which will do bounds checking on every access.
While it is true that C perfectly matches the hardware, it imperfectly matches the rich software abstractions which are needed for moderate and large sized software. The lack of namespaces and sane object creation, destruction semantics etc, makes programming tedious.
A large proportion of code in many large C programs go into recreating imperfectly the features that are by default provided by richer programming systems. And that repeats for every large program you do. It quickly becomes boring and unenlightening, to recreate an exception handling mechanism or data structure implementation for the nth time.
Why would anyone not prefer not having to focus on the mere infrastructure, and rather direct directions on the interesting problems to be solved.
Another thing is that large C code bases tend to become ensconced in layers of preprocessor macros, which I think is a hack-y way of doing things.
It seems like many of the responses to this article are from Rust and other language fans (but mostly Rust). I think many people are reading what they want instead of what the author was trying to convey:
"This is the most important and only real non-negotiable requirement for me; the reliability and portability of the toolchain and the access it provides to system and device interfaces."
Rust isn't exactly reliable. Try to build Rust itself. There are all sorts of issues with resource consumption and parallelism, and it takes more CPU and memory than compiling an entire OS.
Rust's portability is strongly discouraged by the resources it requires. Many platforms REQUIRE cross-compilation because they don't have enough memory, and this makes it hard to say it's easily portable.
If you're a fan, you're easily going to overlook this. You probably don't care that more than half the world doesn't have computers which have the resources to run Rust. That's fine. You also say that people shouldn't be concerned with compiling their own stuff. That's fine, too. But there are people who like to create projects and write software that's as accessible as possible to most people, not just to some people and available to many others just as downloadable binaries.
If you're not the kind of person who wants a free and open software ecosystem where everyone can participate, not just those with the resources to buy fancy, powerful, multigigabyte, multigigahertz, multicore 64 bit CPUs, then Rust is fine for you. If you you care about accessibility to as many people as possible, perhaps you'd come to the same conclusions as the author.
It seems you are working on a different set of constraints than the author's. The author was talking about making games. Those are generally proprietary, and even the free one almost always have binary distributions. Rust itself likely have a binary package for your development platform of choice.
Likewise, cross compilation is likely not a problem. You don't develop on the PS4 or Switch, you develop for them. Sure, in practice, cross compilation in C and C++ is indeed a problem. I'm wondering to this day how that's even a topic. A compiler is just a goddamn translator, the target platform should naturally be independent from the source platform. Worst case, you need to recompile the standard library for the new platform. (I suspect this is related to the bewildering difficulty of linking libc statically.)
You do have a point, but it's much broader and much stronger than choosing C over Rust. We spend heaps and heaps of code solving problems that could be avoided in the first place. Niklaus Wirth's Oberon operating system required less than 10,000 lines of code, and now we need like 200 million lines?! No way we now have 20,000 times the functionality, so we can guess that current computing systems are at least 100 times too big (that is, comprised of over 99% accidental complexity, Brooks be damned).
I sometimes feel the only way to make software truly Free and Accessible (meaning, people have the right and the means and the ability to use & modify their software), is to remake everything from scratch, with the benefit of hindsight.
I wish the myth that C is simple would die. It has all sorts of complex nooks and crannies. Even some basic things like specifying types are quite insane (e.g. for function pointers).
I disagree. Much UB makes the language less orthogonal, and ultimately makes the specification longer. And more complex.
Classic example of illusion of simplicity: signed integer overflow. Going outside the range? Undefined. Left shifting a negative integer? undefined. You can't just assume 2's complement, you have to be aware of the Nasal Demons compilers are becoming increasingly apt at summoning.
Another one: out of bounds array index is undefined, even if there was something of the right type there:
Depending on optimisations and compilers, you won't get the same result every time, and all sanitizers warn you about the UB. Conclusion: you can't assume a flat memory model, even on modern X86 in 32 or 64 bit mode. There are exceptions, depending on how you access memory, through which pointer. Again, your language specs just got longer for not defining what happens outside the bounds.
---
Undefined behaviour simplified one thing: the compilers themselves. At least, until they got the idea to use UB for optimisation, and started to summon the Nasal Demons as a result.
> Classic example of illusion of simplicity: signed integer overflow. Going outside the range? Undefined. Left shifting a negative integer? undefined. You can't just assume 2's complement, you have to be aware of the Nasal Demons compilers are becoming increasingly apt at summoning.
I don't see this as an example of illusion of simplicity. On the contrary. Specifying exactly what must happen on integer overflow or left shift is almost always going to require more than saying "the behavior is undefined." Especially so if you try to keep it portable and allow different behaviors that are natural for different systems. And it probably won't help you if you're trying to write portable code, because now you need to worry about all the defined behavior that might not be what you want.
If you want portable code, then you shouldn't be able to assume a specific behavior, and if you want a language standard that enables the language to perform well across dislike platforms, you can't dictate specific behavior.
If you don't care about portability, you're free to enter a contract between you and your platform & compiler. For example, use -fwrapv. The standard doesn't forbid such a thing. By keeping things simple and leaving them undefined, it explicitly enables you to do things like this without violating what's defined in the standard.
> Another one: out of bounds array index is undefined, even if there was something of the right type there:
That's a very specific scenario. In a lot of cases, there are arrays whose indexes are not right there, not constants (would require range analysis), or array size is not known (but maybe could be inferred). So the standard would have to become more complex in order to specify that something specific must happen in the special case that you're indexing into an array of known size in scope, with a constant index. By simply making out of bounds access undefined, they cover all cases of array access in one short sentence.
Luckily, again, since the standard doesn't require any specific behavior, your friendly implementation is free to invent its own specific behavior, for example issue a diagnostic and return an error code because it saw that you were indexing out of bounds.
> Again, your language specs just got longer for not defining what happens outside the bounds.
Why don't you show how to make the spec shorter by defining exactly what happens in this specific case? Remember that the spec still has to cover other cases, so you can't just delete all the text. You have to add a new case! Post a diff.
Integers represent the binary field of integers modulo 2^b, where b is the width of the integer. For signed integers, any value v >= 2^(b-1) is interpreted as being v-2^b instead.
That's how you specify "exactly what must happen on integer overflow". You thought I would have to take every undefined special case one by one and define them? That's naïve.
A similar argument can be made for pointers: pointers are integers that represent memory addresses. Dereferencing a pointer to an allocated region gives you the value stored in that region (insert casts, uninitialised values, trap representations etc here). Dereferencing a pointer to a region that is not currently allocated is undefined.
And voilà, we no longer care how a pointer was constructed then dereferenced, we only care about the address, and whether it pointed to an allocated region (and initialised with a compatible type etc, though that part could also be simplified).
> That's a very specific scenario.
I'm currently reporting a bug in the Libsodium cryptographic library because its Scrypt implementation (password hash by Colin Percival) has precisely this bug. My guess is that Colin was a little too clever there, and treated C as a lower level language than it actually is.
A very specific scenario that may be, but it's a scenario we do encounter in real life.
> That's how you specify "exactly what must happen on integer overflow".
So yes, like I said, you can force a particular behavior, and throw out any system for which that doesn't come naturally - you just made the language more complex for them. And no, that is not simpler than "the behavior is undefined", just different (and more imposing).
> And voilà, we no longer care how a pointer was constructed then dereferenced, we only care about the address, and whether it pointed to an allocated region (and initialised with a compatible type etc, though that part could also be simplified).
Eh. You did nothing about undefined behavior. It's still there.
> A very specific scenario that may be, but it's a scenario we do encounter in real life.
So what was your proposal that doesn't make the language more complex? Because above, you just gave us undefined behavior, which is our starting square.
> So yes, like I said, you can force a particular behavior, and throw out any system for which that doesn't come naturally
Find me one system in current use (2020) that isn't 2's complement.
> you just made the language more complex for them.
Ah, but I was never talking about implementation complexity. I was talking about specification complexity. Imposing 2's complement makes the specification simpler. No special case, no boundary. Just a binary field. Undefined behaviour draws such a boundary. An extra detail to specify and remember. Some thing that reminds you you're not quite doing modular arithmetic.
> Eh. You did nothing about undefined behavior. It's still there.
Here's the relevant quote from annex J of the (heavy, I have printed it) C99 specifications:
Addition or subtraction of a pointer into, or just beyond, an array object and an integer type produces a result that does not point into, or just beyond, the same array object. (6.5.6)
That's what caused the bug: we were taking a pointer from an array, then used it to overflow past it. It didn't matter that we could access the memory there. What mattered is that we locally overflew that array. Had we computed the pointer by first working with the outer array, then pointed to the right element, we'd have the same pointer, except this time it wouldn't be undefined.
Bonus note: you don't even have to dereference the pointer, computing its value is enough to trigger undefined behaviour.
Here is some more undefined behaviour I have removed (it's subtle, relatively few programmers know this):
Pointers that do not point into, or just beyond, the same array object are subtracted. (6.5.6)
There go flat memory models. Though to be honest, even segmented memory models could define that. Just make the subtraction already, it doesn't have to mean anything. Make it unspecified or implementation defined at least.
Pointers that do not point to the same aggregate or union (nor just beyond the same array object) are compared using relational operators (6.5.8)
That's the reason we can't implement memmove() portably and efficiently. And depending which tool you ask, even converting the pointers to intptr_t first isn't enough. Real world example here: https://github.com/jedisct1/libsodium/commit/e7e378fad116c18...
---
If you haven't already, go read the annex J from the C standard. Ponder the fact that compilers, unless you specifically ask not to (-fwrapv), will exploit every single one, even on platform that could define some reasonable behaviour (2's complement machines). It may hint at how hostile C is to programmers who care about correctness.
Believe me, I know. I wrote a whole Crypto library[1], and that's one of the easiest things to get right, where C is concerned. I mean, the crypto itself is hard, but the total lack of dependency makes it pathologically easy to write in a portable way. Yet C still makes it hard.
> Find me one system in current use (2020) that isn't 2's complement.
Fortunately, C2x will stop with the meme assumption that two's complement hasn't taken over the world (see draft N2479). It will not, however, specify signed integer overflow, which remains UB. :^)
Compiler writers are too hooked on loop optimisations now to give up on undefined signed overflow. I'm afraid the only clean fix is to augment the language with an actual `for` loop, with an immutable loop index and well defined start and finish values. Stuff like:
for (size_t i : 0..10) {} // Will go from 0 to 9 included.
for (size_t i : 10..0, -1) {} // Will go from 10 to 1 included...
// ...or maybe from 9 to 0 included?
// Not sure which is best.
Random note: every few years I update a list [0] of popular programming subreddits (by subscriber count) on the /r/programming wiki. Over the last two years, C increased in popularity overtaking Ruby. And C++ increased in popularity overtaking PHP.
Rust, TypeScript, and Dart made big gains as might be expected. Swift didn't really move anywhere.
The biggest surprise for me were how much the interest in C and C++ continues to grow relative to other high-level languages (that probably also have greater number of professional users).
92 comments
[ 1.4 ms ] story [ 94.7 ms ] threadI was curious about this, but medium wants me to create an account to read it. Oh well.
> You’ve reached the end of your free member preview for this month
The front cover of the “C Programming Language” book as I recall it from memory
Why on earth would someone would pick C to start a new project in 2020? Surely there is a newer language with more shiny features that’s better right? Well I can’t speak for other people but I’ll tell you my reasons.
First of all let me preface this by saying that of course this is a biased opinion and the language I pick for something depends on the context it’s going to be used in. For example; I doubt I’ll ever be reaching for C when writing a web service simply because the ecosystem around that domain isn’t great and I’m not itching to write my http framework at this time.
But for games, more specifically cross-platform games C is a clear winner for me because it provides me with exactly the things I’m looking for which is reliability, simplicity and performance.
Reliability
This is the most important and only real non-negotiable requirement for me; the reliability and portability of the toolchain and the access it provides to system and device interfaces.
Languages, especially dynamic ones and their runtime environments come and go like fashion. When your target platform suddenly gets deprecated from the runtime you’re relying on, well then you’re just fucked and out of luck.
I’ve been in this situation before with multiple languages and I don’t intent to repeat history here so I need something stable, something will have a reliable toolchain available on all the platforms I could ever possibly want in perpetuity without having to worry about compiler bugs as I’m more than proficient in the art of introducing my own bugs.
The ecosystem around C provides all of this. Even for the most obscure platform you can imagine there will be solid stable toolchain with a good set of native libraries that provide access to system and device interfaces for said platform.
Performance
While I will happily write software in slower languages without that much complaint, performance is fairly high up on the list when it comes to writing games.
Avoiding cache misses is key to that, so having any kind of dynamic language with an interpreter is hopeless. Even in the best case scenario that the platform of choice provides you with one of the magical JIT compilers available today, they’re still magical black boxes which makes it difficult to reason about performance characteristics object boxing/unboxing and cache locality.
Same goes for stop the world garbage collection, again there are some fairly impressive implementations out there; Go’s garbage collector comes to mind but it’s still a garbage collector and it will still lead to frame drops. If you have to use object pools to tip-toe around garbage collector killing the frame-rate then what’s the point of having one in the first place.
Even for a low fidelity game filled with chunky pixelated goodness having more cycles gives you a lot of room to experiment and still have plenty of room to add copious amounts of extra juice.
But It’s not just about running fast, battery life matters as-well. Fewer cycles means less battery consumption which is a major win if you ask me. As long as I can help it, I’ll do my best to avoid draining a user’s battery in five minutes.
Lastly, when it comes to the web and mobile the executable size also has to be considered. While it’s not a deal breaker it’s a nice bonus that if you work on it a little bit C executables built with WebAssembly essentially have zero bytes of overhead by default. It might seem like a minor thing but it’s the difference between loading instantly and taking 3–5 minutes to load on a connection.
Simplicity
While it’s not a critical requirement, a nice thing about C is that it is an extremely simple language which is a nice welcome break from working in monolithic languages that get jam packed with new features every time someone on the design team or committee learns about a new feature in another language causing a p...
Please ask yourself this before writing any code (For a cross-platform game).
By just reading this, I thought to myself if I were to write a cross-platform game in 2020, Is C necessary to accomplish this? Perhaps anything C can do, surely Rust can do it safer and arguably better.
In most cases, C would only be more practical on obscure or outdated hardware, though.
96% of the Linux kernel is written in C (https://github.com/torvalds/linux).
Update: Got to my system so I could actually look at available files accurately:
Edits: Update section and removing "100% in C" and "5 sources files of assembly"The counter reports 1.2%, but if you click on that to show Assembly files it only reported 5 code results (it's not actually showing all Assembly, so you then have to click on "Unix Assembly" on the side).
> Reliability
While C is indeed a winner for obscure platforms, many language gets you perfectly covered for a "cross-platform" game.
> Performance
You can get equally good performance in Rust and C++.
> Simplicity
As he says, simplicity does not mean easy nor intuitive. Then it is a matter of opinion why simplicity should be a good thing. Personally, I prefer a complex but easier to use language than a simple but difficult to use language.
That's not to say you couldn't cobble together a solution with enough elbow grease and FFI, but the cure may be worse than the disease.
[1] https://wiibrew.org/wiki/List_of_development_tools
1. Rust is very slow to compile. This is bad when rapid iteration is important.
2. Rust is a complex language. It wants to replace C++, not C, and gladly adopts features™ and the complexity that comes with that.
This is a good point. I wonder if the trend of saying C/C++ is to blame; that is, people see C, think C/C++, and then are incapable of realizing that C is, in fact, its own language, and not merely "C++ Jr." or C++ with some features removed.
There's a hole in the modern language world, from what I can see: We lack a simple, mid-level language which incorporates modern knowledge about language design, where by mid-level I mean that all resources (other than stack, perhaps) are managed by hand but it's reasonably simple to write cross-platform code.
github.com/glouw/openempires
and my time spent with C has really just been manually implementing min heaps, circular buffers, ad hoc vectors, and a tier 1 dummy garbage collector.
All this stuff is readily available in C++, and I could have saved hundreds of hours by just using C++.
But, to one benefit, since I did all this work myself, I understand the machine better, and my compilation times are orders of magnitude faster than what I would have had if I chose C++. Additionly, since so few calls are even being made to the C standard library, the majority of the code exists in text, and easily inlines across all translations units with link time optimization. With C++, as except for templated library code, the majority of link time optimizations go to waste as calls to the standard library are binary linked.
Maybe he's only writing an ascii game though.
I think since none of his reasons include I am doing system programming
I will debunk
Reliability: C is not the only language with a reliable ecosystem, and what should matter most is the reliability of the program, not the tools made to create the program, so in this regard, I think Rust, OCaml, C#, Java are all as reliable, and I would argue it is easier to write more predictable or reliable programs in them
Performance: Yes sure C wins almost every performance benchmark, but Rust is very Fast and Ocaml is also very fast so unless you are working on system programming stuff, I think you have better options that are more reliable and predictable
Simplicity: Here I would like to contrast simplicity of the tool vs simplicity of the product, complexity does not go away, complexity is abstracted, simple tool = complex product or process, complex tools on the other hand may allow you to create simpler tools and processes, if you use simple tools you will transfer complexity to your products, which I don't think is necessarily a good thing
I was thinking about languages in terms of ecology. I'm a Python fan and while its lack of performance never bit me in the real world, how many Joules would be saved if my apps were written in another language ? I'd love to see a study/comparison based on real apps (not algos implementations).
https://thenewstack.io/which-programming-languages-use-the-l...
Edit: just saw sibling comment linking to the same paper.
Sage advice.
Would you say PHP is in fashion? How about Perl or Tcl?
Did you read what the OP quoted? Here it is:
> Languages, especially dynamic ones and their runtime environments come and go like fashion.
C has everything, including several top quality http frameworks. I use https://github.com/corvusoft/restbed and find it more reliable, more performant, and "better" in all respects than the php, js, node, and whatever frameworks I've been forced to learn.
There's also so much FUD about performance, but reality is that without numbers, you'll never know. Certainly for game development, unless you're working on a cutting-edge game engine, there's little reason to focus on performance first, and even then - most popular modern game engines offer interpreted paths for game logic.
Fear of garbage collectors is also bullshit in most cases. Many don't realise a simple call to free or malloc (or new/delete in C++) can result in just as much unpredictable hick-ups, which is why many games engines use specially tuned memory managers, some even specifically including garbage collectors (see the Unreal Engine for example).
Certainly in game development, flexibility to tune game mechanics is far more important to make a good game than performance, and creating such flexibility in a C application is a lot more complex/more work than in a higher-level language, and you'll end up building very high walls you really don't want to tear down and start over with.
With emphasis on "can". Not that you should. This is exactly what I mean, in many cases it's interesting for an intellectual exercise, but I would avoid it for new projects unless I really can't.
Rust is still a decade or more away from anything serious like C. Majority of the functional Rust libraries today depend on underlying C libraries.
Rust may try to replace C++ but replacing C is far away dream.
Let's wait when Mozilla (origin and major supporter for Rust language) can release Firefox in Rust, until than it's still a popular experiment in systems programming area.
Rust does not run any critical component like Linux Kernel today, when it will be able to do it than it can be considered as real systems programming language challenger to C. Right now most of it's safety guarantees are not really battle tested like C.
> What do you think of the projects currently underway to develop OS kernels in languages like Rust (touted for having built-in safeties that C does not)?
> That's not a new phenomenon at all. We've had the system people who used Modula-2 or Ada, and I have to say Rust looks a lot better than either of those two disasters.
> I'm not convinced about Rust for an OS kernel (there's a lot more to system programming than the kernel, though), but at the same time there is no question that C has a lot of limitations.
> I spoke with Greg Kroah-Hartman, and he said he'd be willing to accept a framework in the kernel for writing drivers in Rust, as long as 1) for now it wasn't enabled by default (even if you did "make allyesconfig") so that people don't need Rust to build the kernel, and 2) it shows real benefits beyond writing C, such as safe wrappers for kernel APIs.
https://lwn.net/Articles/797828/
Hmm, maybe I should have written "...for general kernel code" or "...for most kernel code", that would have been clearer.
On what basis do you say this?
> Let's wait when Mozilla (origin and major supporter for Rust language) can release Firefox in Rust,
Firefox has shipped rust code since August 2016 with the release of Firefox 48.
> Rust does not run any critical component like Linux Kernel today,
This depends on how you define "critical component." At this point, there is a lot of Rust in a lot of critical pathways for a lot of things.
Incidentally, the kernel maintainers have suggested that they're open to the possibility of Rust in optional drivers. See my other comment below.
[1] https://www.rust-lang.org/what/embedded
[2] https://github.com/rust-embedded/embedded-hal
[3] https://www.tockos.org/
[4] https://www.tockos.org/blog/2020/hello-opensk/
What does that entail exactly? And why is it a "good thing" to be close to the hardware even if we were to accept that statement?
In reality I use anything that gets the job done faster. That usually means prototyping in C++,lisp, python, swift, java, perl, lua.
After I have the prototype I code it automatically with C, or minimal C++ and my own DSLs, done in Lisp controlling C, C++ code generation.
I have reinvented the wheel a lot. Someone told me you are not going to be smarter than a compiler, but having read several of them, they are so dumb, and they have to be because they are generic.
E.G In a project I was using std::vector and predicted the expected performance of the whole program by O notation and "on the park" calculation needs as X.
When I completed the program, it was X/1000. Fair enough I made some stupid mistake...in the end I got X/100 performance and could not optimize farther.
I got so frustrated so I started replacing components to see if numbers improved. Replacing std::vector by a c vector made the program X/1.4!!
So I started studying std::vector implementation and it was a template of a template of a template... I could not wrap my head around such a mess.
My implementation was done in lisp with proper macros and was way simpler and efficient and generated c code, that was so easy (although tedious) to understand.
By comparison, glibc isn’t so bad to read. Same with the standard library for Python, Go, Rust, etc… I have never had such a problem reading library source code as I have with the C++ standard library.
[1] https://zeux.io/2019/01/17/is-c-fast/
Someone else noted that debug more will stifle performance, which is true and the most likely culprit.
Other problems can include constant resizing and memory allocation and copying, but when something is 1/100th the speed it should be, that screams debug mode being left on, which will do bounds checking on every access.
Thanks.
[1] https://github.com/kiselgra/c-mera
A large proportion of code in many large C programs go into recreating imperfectly the features that are by default provided by richer programming systems. And that repeats for every large program you do. It quickly becomes boring and unenlightening, to recreate an exception handling mechanism or data structure implementation for the nth time.
Why would anyone not prefer not having to focus on the mere infrastructure, and rather direct directions on the interesting problems to be solved.
Another thing is that large C code bases tend to become ensconced in layers of preprocessor macros, which I think is a hack-y way of doing things.
"This is the most important and only real non-negotiable requirement for me; the reliability and portability of the toolchain and the access it provides to system and device interfaces."
Rust isn't exactly reliable. Try to build Rust itself. There are all sorts of issues with resource consumption and parallelism, and it takes more CPU and memory than compiling an entire OS.
Rust's portability is strongly discouraged by the resources it requires. Many platforms REQUIRE cross-compilation because they don't have enough memory, and this makes it hard to say it's easily portable.
If you're a fan, you're easily going to overlook this. You probably don't care that more than half the world doesn't have computers which have the resources to run Rust. That's fine. You also say that people shouldn't be concerned with compiling their own stuff. That's fine, too. But there are people who like to create projects and write software that's as accessible as possible to most people, not just to some people and available to many others just as downloadable binaries.
If you're not the kind of person who wants a free and open software ecosystem where everyone can participate, not just those with the resources to buy fancy, powerful, multigigabyte, multigigahertz, multicore 64 bit CPUs, then Rust is fine for you. If you you care about accessibility to as many people as possible, perhaps you'd come to the same conclusions as the author.
Likewise, cross compilation is likely not a problem. You don't develop on the PS4 or Switch, you develop for them. Sure, in practice, cross compilation in C and C++ is indeed a problem. I'm wondering to this day how that's even a topic. A compiler is just a goddamn translator, the target platform should naturally be independent from the source platform. Worst case, you need to recompile the standard library for the new platform. (I suspect this is related to the bewildering difficulty of linking libc statically.)
You do have a point, but it's much broader and much stronger than choosing C over Rust. We spend heaps and heaps of code solving problems that could be avoided in the first place. Niklaus Wirth's Oberon operating system required less than 10,000 lines of code, and now we need like 200 million lines?! No way we now have 20,000 times the functionality, so we can guess that current computing systems are at least 100 times too big (that is, comprised of over 99% accidental complexity, Brooks be damned).
I sometimes feel the only way to make software truly Free and Accessible (meaning, people have the right and the means and the ability to use & modify their software), is to remake everything from scratch, with the benefit of hindsight.
Then you learn about Undefined Behaviour.
Do not confuse simple with easy.
Classic example of illusion of simplicity: signed integer overflow. Going outside the range? Undefined. Left shifting a negative integer? undefined. You can't just assume 2's complement, you have to be aware of the Nasal Demons compilers are becoming increasingly apt at summoning.
Another one: out of bounds array index is undefined, even if there was something of the right type there:
Depending on optimisations and compilers, you won't get the same result every time, and all sanitizers warn you about the UB. Conclusion: you can't assume a flat memory model, even on modern X86 in 32 or 64 bit mode. There are exceptions, depending on how you access memory, through which pointer. Again, your language specs just got longer for not defining what happens outside the bounds.---
Undefined behaviour simplified one thing: the compilers themselves. At least, until they got the idea to use UB for optimisation, and started to summon the Nasal Demons as a result.
I don't see this as an example of illusion of simplicity. On the contrary. Specifying exactly what must happen on integer overflow or left shift is almost always going to require more than saying "the behavior is undefined." Especially so if you try to keep it portable and allow different behaviors that are natural for different systems. And it probably won't help you if you're trying to write portable code, because now you need to worry about all the defined behavior that might not be what you want.
If you want portable code, then you shouldn't be able to assume a specific behavior, and if you want a language standard that enables the language to perform well across dislike platforms, you can't dictate specific behavior.
If you don't care about portability, you're free to enter a contract between you and your platform & compiler. For example, use -fwrapv. The standard doesn't forbid such a thing. By keeping things simple and leaving them undefined, it explicitly enables you to do things like this without violating what's defined in the standard.
> Another one: out of bounds array index is undefined, even if there was something of the right type there:
That's a very specific scenario. In a lot of cases, there are arrays whose indexes are not right there, not constants (would require range analysis), or array size is not known (but maybe could be inferred). So the standard would have to become more complex in order to specify that something specific must happen in the special case that you're indexing into an array of known size in scope, with a constant index. By simply making out of bounds access undefined, they cover all cases of array access in one short sentence.
Luckily, again, since the standard doesn't require any specific behavior, your friendly implementation is free to invent its own specific behavior, for example issue a diagnostic and return an error code because it saw that you were indexing out of bounds.
> Again, your language specs just got longer for not defining what happens outside the bounds.
Why don't you show how to make the spec shorter by defining exactly what happens in this specific case? Remember that the spec still has to cover other cases, so you can't just delete all the text. You have to add a new case! Post a diff.
That's how you specify "exactly what must happen on integer overflow". You thought I would have to take every undefined special case one by one and define them? That's naïve.
A similar argument can be made for pointers: pointers are integers that represent memory addresses. Dereferencing a pointer to an allocated region gives you the value stored in that region (insert casts, uninitialised values, trap representations etc here). Dereferencing a pointer to a region that is not currently allocated is undefined.
And voilà, we no longer care how a pointer was constructed then dereferenced, we only care about the address, and whether it pointed to an allocated region (and initialised with a compatible type etc, though that part could also be simplified).
> That's a very specific scenario.
I'm currently reporting a bug in the Libsodium cryptographic library because its Scrypt implementation (password hash by Colin Percival) has precisely this bug. My guess is that Colin was a little too clever there, and treated C as a lower level language than it actually is.
A very specific scenario that may be, but it's a scenario we do encounter in real life.
So yes, like I said, you can force a particular behavior, and throw out any system for which that doesn't come naturally - you just made the language more complex for them. And no, that is not simpler than "the behavior is undefined", just different (and more imposing).
> And voilà, we no longer care how a pointer was constructed then dereferenced, we only care about the address, and whether it pointed to an allocated region (and initialised with a compatible type etc, though that part could also be simplified).
Eh. You did nothing about undefined behavior. It's still there.
> A very specific scenario that may be, but it's a scenario we do encounter in real life.
So what was your proposal that doesn't make the language more complex? Because above, you just gave us undefined behavior, which is our starting square.
Find me one system in current use (2020) that isn't 2's complement.
> you just made the language more complex for them.
Ah, but I was never talking about implementation complexity. I was talking about specification complexity. Imposing 2's complement makes the specification simpler. No special case, no boundary. Just a binary field. Undefined behaviour draws such a boundary. An extra detail to specify and remember. Some thing that reminds you you're not quite doing modular arithmetic.
> Eh. You did nothing about undefined behavior. It's still there.
I did. There's less of it. I've filled the bug, here's what's this about: https://github.com/jedisct1/libsodium/issues/937
Here's the relevant quote from annex J of the (heavy, I have printed it) C99 specifications:
Addition or subtraction of a pointer into, or just beyond, an array object and an integer type produces a result that does not point into, or just beyond, the same array object. (6.5.6)
That's what caused the bug: we were taking a pointer from an array, then used it to overflow past it. It didn't matter that we could access the memory there. What mattered is that we locally overflew that array. Had we computed the pointer by first working with the outer array, then pointed to the right element, we'd have the same pointer, except this time it wouldn't be undefined.
Bonus note: you don't even have to dereference the pointer, computing its value is enough to trigger undefined behaviour.
Here is some more undefined behaviour I have removed (it's subtle, relatively few programmers know this):
Pointers that do not point into, or just beyond, the same array object are subtracted. (6.5.6)
There go flat memory models. Though to be honest, even segmented memory models could define that. Just make the subtraction already, it doesn't have to mean anything. Make it unspecified or implementation defined at least.
Pointers that do not point to the same aggregate or union (nor just beyond the same array object) are compared using relational operators (6.5.8)
That's the reason we can't implement memmove() portably and efficiently. And depending which tool you ask, even converting the pointers to intptr_t first isn't enough. Real world example here: https://github.com/jedisct1/libsodium/commit/e7e378fad116c18...
---
If you haven't already, go read the annex J from the C standard. Ponder the fact that compilers, unless you specifically ask not to (-fwrapv), will exploit every single one, even on platform that could define some reasonable behaviour (2's complement machines). It may hint at how hostile C is to programmers who care about correctness.
Believe me, I know. I wrote a whole Crypto library[1], and that's one of the easiest things to get right, where C is concerned. I mean, the crypto itself is hard, but the total lack of dependency makes it pathologically easy to write in a portable way. Yet C still makes it hard.
[1] https://monocypher.org
Fortunately, C2x will stop with the meme assumption that two's complement hasn't taken over the world (see draft N2479). It will not, however, specify signed integer overflow, which remains UB. :^)
Rust, TypeScript, and Dart made big gains as might be expected. Swift didn't really move anywhere.
The biggest surprise for me were how much the interest in C and C++ continues to grow relative to other high-level languages (that probably also have greater number of professional users).
[0] https://www.reddit.com/r/programming/wiki/faq?v=0692c0d1-617...