Why I’m Using C
medium.com
medium.com
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.
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
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.
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.
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
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.
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:
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"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. 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.
I 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 paradigm shift until the next iteration which causes another paradigm shift leaving the whole ecosystem in a bit of a tangled mess.
When I’m saying C simple I don’t necessarily mean easy. If you don’t know what you’re doing C will absolutely blow up in your face and make you spend the day trying to figure out what you did wrong. But again it’s a simple language so it’s really not too hard learning how to write well-behaved programs. Secure programs is a different matter but well-behaved programs are easy-enough.
Now there’s definitively features that would be nice to have in C; I wouldn’t hate having quality of life things like arrays that don’t decay to naked pointers, modules or deferred statements for example but not having them aren’t total deal breakers.
At the end of the day I prefer having a limited surface area in the language than having a language that gets in the way bringing absurd amounts of complexity and bad abstractions to the problem.
This applies to any language in theory but certain languages encourage you a-lot more to be “clever” more than others. I prefer to be pragmatic because if one is trying to be clever when writing a program, then good luck because one needs to be twice as clever debugging and maintaining it in the long run.
>
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.
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:
#include <stdio.h>
#include <stdint.h>
typedef struct {
uint64_t a[8];
} blk;
int main()
{
blk b[2];
for (size_t i = 0; i < 16; i++) {
b[0].a[i] = i;
}
printf("normal : %lu\n", b[1].a[2]);
printf("overflow: %lu\n", b[0].a[10]); // undefined
return 0;
}
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.
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. :^)
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.Maybe he's only writing an ascii game though.
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...
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.