I'd consider myself the former: I use C because it's the only viable option for what I'm doing - but most of the time it's nothing to do with C itself, but the various extensions and builtins of GCC. It's the inline asm, the control of registers, placement of code and data, control of inlining, etc, which are missing from all of the C "replacements". The replacements assume you are building a user application on top of an existing unix-like OS or VM, but they're missing the parts necessary to write the OS or VM itself. They don't provide simple ways to use hardware-specific features.
Obviously there is C++ which can leverage most of this too, but C++ traps you into an ABI which is difficult to use from any language which is not C++.
I also consider myself the former. I often use C not so much to stay close to the hardware, but to be closer to the operating system APIs. Of course, there are often API bindings for other languages, but they're often very incomplete, buggy, non-idiomatic, non-ergonomic, or lacking performance. And IME you only find this out after a few months. But from the get-go they're always worse documented and less flexible. I tend to mix C with other languages, like Rust or Python. This is mostly for audio and graphics stuff.
With that said: Swift replaced C (and Obj-C) 100% in the Apple world for me. Because the API bindings are as good.
"register" is still supported by GCC for combined use with asm. eg, `register uintptr_t x asm ("edx");`
Does D support anything like `__attribute__((section("t1")))`, so we can have the linker decide how to locate some compiled code, or computed gotos?
Can we use D's inline assembler to clobber registers?
I've had trouble even with clang and extended asm. For example, LLVM does not support the "g" constraint.
I understand that there aren't many use-cases for these specific features, but I have an unusual direct-threaded VM which relies on some of them, and the only real alternative I see is to write plain assembly.
I've been using D for OS development and have found it very good for controlling low-level details. GDC is the GCC frontend for D, and has most/all of the same features for controlling this stuff as GCC. For example, you can use `@register("edx") ulong x;` to specify that a variable should be in a particular register, `@section("t1")` on functions to place them in certain sections, and the inline assembler is the same as with GCC. Note: the @register feature is new with GDC 13. See here for the docs: https://gcc.gnu.org/onlinedocs/gdc/.
And of course LDC supports all the LLVM custom attributes, plus GCC-compatible inline assembler (not sure about the "g" constraint though) and LLVM-style inline assembler.
Thanks for the information. Appears that it does support the section attribute from GCC too. I'll have to explore this a bit and see if I can use it, since it seems to have all or most the features I use.
I did learn D some years ago when the standard library situation was not great, but might be worth looking at again.
Depending on how much language support you want, you may want to compile without the D runtime (in which case you only have access to the C standard library, and various features are disabled, such as classes/interfaces, garbage collection, exceptions, and most of the D standard library). You can disable the D runtime in GDC with -fno-druntime and in LDC with -betterC. With those flags, the basic hello world program looks like this:
The register support you describe is an extension, not really part of C. Gcc has lots and lots of C extensions, but they don't make the language simple.
D's inline assembler does register management for you, i.e. it tracks register usage through the instructions, so you don't have to say which registers are read or written to.
D does not support the "section" extension. I understand that certain gcc C extensions are needed for special purposes. Gcc may be the most suitable language if you need those extensions.
What I would do is use gcc for the parts of the code that need those extensions, and D for the rest (D code can directly interact with C code).
Thanks. My root comment mentioned that it was not really the features of C that I depended on, but the GCC specific features.
I'm interested in using D now as sibling comment mentions that I can use some of these features in gcd, but I'm not yet sure how much benefit I'd get over using C by using a subset of the D features which are compatible with the constraints of my VM.
The (experimental) VM I'm working on embeds type information into pointers. I place some functions at fixed virtual addresses and use the type information from the pointer to materialize these addresses at runtime, without having to dereference any pointers until I actually call the function. Essentially, if you have a pointer you will know the type of value it points to from the pointer itself.
This method places some tight constraints on how memory can be allocated, but I don't think it will be too much of a limitation for most applications intended to run on it. I have 12-bits in a 48-pointer which provide type information, which leaves a maximum 36-bits of virtual address space per type (or 35 bits if you discount the most significant bit which refers to kernel space).
I'm currently using the section attribute to implement it, but I'm aware there are other methods to achieve this. I could do it at runtime by `mmap`ing the virtual memory and then loading in the machine code at the addresses I need. This method might be more flexible in the long run and would free me up from using GCC specific attributes.
> Obviously there is C++ which can leverage most of this too, but C++ traps you into an ABI which is difficult to use from any language which is not C++.
To be fair to C++, all the other languages in this space are as bad at providing ABIs in their own languages. All (almost?) mostly provide ways to declare C ABIs, which C++ also supports.
C++ has rough C++ ABIs, but mostly because it bothers to try in the first place. It's not like other languages really "solved" ABI better than C++ has.
Even C ABIs are a little rough since there's so much preprocessor use to factor in and/or avoid.
C's ABI is that much simpler because it doesn't really place any constraints on the memory layout of your types. With C++ (and others), the values passed around may also have vtables linked to them, so your language must then also provide an in-memory representation of objects which is compatible with the C++ representation in order to do FFI method calls. That places quite a constraint on your language because to do it efficiently you basically want a copy of the C++ object model. So you're developing a kind of C++ sibling language, and C++ is quite complex, and now any other languages which want to interoperate with yours has to do the same.
This is why it's pretty standard to just have a C FFI and provide a C wrapper for C++ libraries to use them with other languages.
Of course the C ABI places constraints on the memory layout of the types. For example, alignment. Also in what register "small" types are passed when passing them to functions. It actually depends if the struct has float members or not.
C itself is quite complex already.
C++ adds more features.
But for example, the Rust ABI uses the C++ ABI, because C is too simple and doesn't support unwinding (for panics) or 128bit integers
The use of trademark enforcement to coerce conferences into certain policies (which might contradict local law) is a deal breaker. It also demonstrates a willingness of the foundation to use wield power for purposes completely unrelated to the language.
There is no such things about forcing policies for conferences in the current trademark policy. (You're getting confused with a proposal that is being re-worked)
And even if there was it's hardly a deal breaker to use the language.
The proposed policy, which is clearly political in nature, has resulted in our firm decision not to adopt the crab language for our projects. Although Rust™ offers valuable features, introducing politics into the equation was both unnecessary and has caused a rift that may prove difficult to mend.
so then, add a declarator or two to solve the problem, don't just throw the baby out with the bathwater. The population of C programmers who understand how C works is the population of people who can understand how hardware works. The reverse has never been true, hardware designers have never understood software, and academic software experts have spent too much time babysitting college freshman who can't code and spend all their time dreaming about languages that freshmen could learn well enough to TA the course. But that's probably not a language good enough for systems programmers to adopt.
> C is not aligned with modern hardware and optimizing compilers are severely hampered by things like undeclared pointer aliasing.
Which language is?
I mean, if you're going to parrot this line, surely you have an example of a language that is more closely aligned to hardware than C, right?
I see this line repeated in every HN thread about C. It's not a new sentiment, but it is mostly wrong because the implication is that there exists some other popular language that actually aligns with the hardware better than C does.
A language can not be exactly aligned to every possible hardware variation at the same time of course. So C does dome abstracting from the hardware level but keeps the mental model of memory locations which can be pointed to by pointers.
> A language can not be exactly aligned to every possible hardware variation at the same time of course. So C does dome abstracting from the hardware level but keeps the mental model of memory locations which can be pointed to by pointers.
Sure, and I agree with you, but anyone complaining that the language which models hardware more closely than any other language, "doesn't model the hardware" should be prepared for the followup question of "Well, which other language in common use is closer to the hardware?"
It's really tiring reading the same old assertions in every thread about C; usually backed up by the same two or three sources that everyone has already read.
> The System V ABI is closer to being a standard than some official standards are.
That brings back some unpleasant memories. What happens when a popular compiler decides that whatever they choose is the ABI, and the published spec be damned? https://gcc.gnu.org/bugzilla/show_bug.cgi?id=38496
Some people like to workship whatever the UNIX founders have done, yet they miss that for Plan 9 and Inferno, they also decided to go with automatic memory management languages.
While Alef failed, Limbo's design with GC was considered a revisit from what was missing from Alef.
They also miss that lint was considered a must have tool for safer C code, introduced in 1979, and that Dennis actually proposed fat pointers to ISO, which weren't accepted.
Maybe, but when all the winning OSes share a common characteristic then you cannot simply dismiss that characteristic as irrelevant to winning without some evidence that it is not relevant.
"Plan 9 failed simply because it fell short of being a compelling enough improvement on Unix to displace its ancestor.
Compared to Plan 9, Unix creaks and clanks and has obvious rust spots, but it gets the job done well enough to hold its position.
There is a lesson here for ambitious system architects: the most dangerous enemy of a better solution is an existing codebase that is just good enough."
By install count, Linux runs on far more platforms than anything else: smartphones, tablets, servers, virtual machines, embedded devices, supercomputers, and spacecraft.
They may not technically be free, but they typically come bundled with the computer you buy. Linux has yet to be shipping with free comparable computers, so the cost of Windows/MacOS is invisible to the end consumer.
C programmers are implicitly converted to the required type.
Implicit conversions are one thing I don't like about C, or at least the way it ended up as int grew (maybe it would have been better if ANSI had picked unsigned-preserving, but maybe that's just grass-is-greener thinking). I also don't like how UB turned into “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs.”
I do like the directness and explicitness. Good C can be very nice.
> I also don't like how UB turned into “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs.”
You can have a language where all the behaviour is defined but in my experience most programmers who whine about this in C are just as unhappy because what they actually wanted was DWIM. In a language with defined behaviour their program is wrong, which is their fault, whereas in C they can claim "obviously" it's the compiler's fault.
I like it when it's my fault, I just learned something, and early in my education I assumed everybody was the same but I rapidly learned that most people prefer to be told they're correct regardless, and a language which says "Nope, 4 - 5 is a negative value and this variable in unsigned, too bad" makes those people unhappy, whereas when 4 - 5 = mysterious program behaviour they can blame the compiler engineers instead.
4u - 5u is not undefined behavior in c; unsigned arithmetic overflow is defined to wrap around, in both directions
assigning -1 to an unsigned variable has never been undefined behavior either, but implementation-defined behavior (see §6.2.1.2p3 in iso c90 or §6.3.1.3 in c99 and c11); decent compilers like gcc define it to do the obviously correct thing (see https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.h...), not produce mysterious program behavior. in the new c++20 standard it's no longer even implementation-defined, but fully define behavior in the standard, but in standard c it's still implementation-defined
as a programmer who whines about the kind of ub exploitation at issue here, and who (as established above) has a much clearer idea than you do of what behavior is and is not defined, specifically what i am whining about is that it's harder to debug my code if doing things like dereferencing a null pointer doesn't reliably crash it, but instead triggers the silent removal of apparently unrelated logic from my program, or subtle semantics changes in it, in the name of optimization
also it sucks to have existing security-sensitive code acquire new security holes, and i don't really care if that's a learning opportunity for the guy who wrote it 20 years ago
this didn't happen 20 or 30 years ago, and in those 20 or 30 years the impact of introducing fresh security holes into existing well-tested c code has greatly increased, while the usefulness of c compiler optimizations has greatly decreased
You're correct that 4 - 5 isn't undefined in C. But there's a good reason that Rust, for example, chooses not to make its built in integer types (either signed or unsigned) wrap on overflow. Wrapping<u32> is very easy for a typical processor to implement but in most cases overflow is a mistake, so you want to report it not silently give the wrapping answer instead when the programmer apparently hasn't considered overflow at all.
If you give up the aggressive optimisations then C is even less competitive with modern languages, so you're basically arguing that it's acceptable to have code that's harder to write, more buggy and with worse performance just because that's how you'd have done it 30 years ago. At that point you're talking about a retro hobby.
what i'm arguing is that c compilers exist primarily to support the base of existing c code and to run it correctly and with adequate performance, not out of some unhinged notion of 'competitiveness' or to entice people into writing new programs in it
that is, i do think it's acceptable to have code that's harder to write and with worse performance, but not because of some hypothetical; rather, it's because that's how people did do it 30 years ago, and 20, and sometimes 10, and i want to run the code they wrote. this could be described as a 'retrocomputing hobby' except that that code is, for example, linux, cpython, gcc, r, libreoffice, gtk, glib, grub, bash, freetype, emacs, vim, libjpeg, libpng, postgres, mariadb, ssh, openssl, gnutls, apache, poppler, ghostscript, tex, gpg, zlib, memcached, redis, etc.
if you want to describe running vim and bash and cpython on linux as a retrocomputing hobby, i guess that's kind of defensible actually, but it comes across as wishful thinking when we don't have a working non-retro alternative yet
(moreover i have no idea why you think omitting these risky optimizations makes c code 'more buggy')
i agree that there are numerous deficiencies in c's semantics, and plausibly silent wraparound is one of them, though it seems like rust's alternative is that overflow crashes in debug builds and silently wraps like c in release builds
I think you need to reevaluate what happens to code once optimizations are turned on. Especially if you can afford to do test guided PGO combined with LTO.
There is nothing in common between how code is executed with optimization and what were programmers writing years ago. It is like 5-10 levels of functions are just gone and completely restructured by compiler. I do not see middle ground if people want to keep the performance. Old C++ and likely C that failed to optimize is just gone at the moment.
Whatever legacy code has to be "preserved" is likely to be specifically deoptimized to keep it in frozen state.
PS I come from gamedev and I really wish compilers left us control of what is going on there.
inlining code and most code-movement optimizations are among the many optimizations that don't require the kind of nasty tricks with undefined behavior that i'm criticizing, and it's been common practice for decades; c++ has depended on method inlining to get acceptable performance since the 80s
it's kind of a pain if you're stepping through the code in gdb but that's acceptable
turning off optimization entirely is still too costly for most cases
That is an interesting point. I wonder if it would be feasible to go over every little bit of UB in the Standard and see if it could instead be well-defined in a way that would be compatible with most existing C code out in the wild. How much perf overhead that would actually be? And is there a way to quantify how many exploits that would have prevented?
this was dan bernstein's project with 'boring c' and john regehr's with 'friendly c'; i think the answer is that most of it can be, but definitely not all, and possibly on some platforms it wouldn't be most existing c code
To be clear, I'm thinking specifically from the perspective of making existing legacy code less broken, not about making the language "more friendly" - as you rightly point out, C really ought to be considered legacy by now (and in new languages, where we will also have this discussion, we can at least begin it from a clean slate). Thus there's no need to consider usability. I was also imagining a hard rule: if the only reason why something is UB is perf overhead, it must not be UB (this covers e.g. the memcpy/memmove debate brought up in that article). But, yeah, as the other example with shifts illustrates, even such tight-fisted approach can be problematic.
The real question is how much perf overhead this entails vs how much benefit from mitigating yet-undiscovered issues caused by UB in legacy code.
The original intent was that “Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose.” If you wrote ‘a + b’, you could expect your hardware's add instruction, and if that happened to trap on overflow, that wasn't the compiler's fault.
The post is kind of confusing regarding which sort is the "first sort", because they are presented in one order in the provocative thesis, and in the other order in the following paragraphs.
But:
> My perception of the second sort of C programmer is that if they've moved to any more recent mainstream language, it's probably Rust.
If in this context the "second sort" is the type who likes C on its own merits (rather than because they don't have any other choice), I think Zig is a much better option than Rust.
I've always thought of myself as the second type of C programmer... I only used C if no other option was available (yeah, it was indeed fear!).
After I started programming in Zig, I realized I really appreciated all of C's simplicity and (lack of) features, but now in "Zig fearless mode", it makes me feel like I always belonged to the first group.
I don't get enjoying every second line being "if (r == -1) goto cleanup" and then a cleanup block freeing every pointer which is not null. True, you can define macros to automate that, but then you just end up with clunkier version of C++ destructors. The rest depends on what you are doing, no need for heavy OOP for simple tasks and on the other hand automatic memory management is great when reliability is more important than hardware access or real time performance. But at least no reason to avoid automated facilities that replace well known red tape.
- there doesn't have to be "two kinds". Trivially you can fit both "types" at once, I certainly feel that way. And if there are 100 programmers, there'd be about 237 other reasons to use a language. False dichotomy.
- a better categorization might be "there are two kinds of C programmers: those who eventually start using rust and those that don't." which is at least of course absolutely true.
- I dislike the idea that a programmer is bound to a language. He's a "C" programmer, She's a LISP person, etc. A good software engineer should use about 5-10 different languages appropriate for the purpose at hand.
I disagree that enjoying C automatically means you will like Rust. If you are obsessed with safety, Rust will be a nice solution. But Rust is a much more complex language than C, which will turn many C programmers away.
Sure it seems so when you start. K&R and away you go, that programming is a lot easier than writing rust sure.
But unless you are writing for just yourself, no current C software get developed that way. You need to understand C standard, that is quite complex and you need to understand it in detail because of undefined behavior (and of course how your compiler interprets it and sometimes fight with compiler to emit right code).
Then you needed to know all the gotchas in the standard library, and how you properly use them. Then you need to know how to do basic arithmetic without having undefined behavior (that's where a lot of memory leaks happen). Or maybe the software you are working on has created their own implementation of most of those functions, so you need to figure how to use those. Even to just print out a debug statement can sometimes be nontrivial.
And of course you need a way to compile the damn thing, just figuring out what sane options you need to use to get decent warnings and what they mean takes a while.
And when you figure all of that out, you still need to keep track of most of the stuff that borrow checker does, but this time by hand using comments and conventions (that you also need to learn for each project.)
(I left out whole build setup, but its usually harder than Rust one too. )
I believe that programmer who has only programmed java or maybe C# before (or python, js...) will get up too up to speed faster on Rust than on C on most real-world projects
C is simpler so long as you avoid "smart" code and stick to simple patterns consistently, like if/goto error cleanup or using libraries to deal with strings. It's not hard, just very tedious. Someone coming from Java or C# would quickly recognize that this is basically doing what they've taken for granted, but manually. Once they do, so long as they have the discipline and patience to stick to this approach, it's not really all that different. GObject is a good example of this kind of C.
With Rust, you have to think very differently about how to e.g. structure your data to make the borrow checker happy in many cases. This doesn't really translate to anything similar in other mainstream languages; it's a whole new skill that has to be learned.
> I dislike the idea that a programmer is bound to a language. He's a "C" programmer, […]
Nothing in the article suggests that being a "C programmer" is that kind of a binding label. It's you reading that into it ;). (Though certainly influenced by the fact that that is a widespread interpretation.)
FWIW, I consider myself a C programmer, and in both camps presented in that article. But I'm also a Python programmer, and I don't think either of those two languages "owns" me.
Dunno, I'm a self-professed "java" programmer even though at my current and prior job there is not a single line of Java code. (Current job is golang and python, prior job was python and scala).
But many of my side projects are Java and she is always first in my heart no matter where the money takes me.
Rust is from a different language family, but I think it still fits a taste of "low-level control, not OOP, not C++".
I know many people see "complex with angle brackets" and equate Rust more with C++, but I disagree and think it's still closer to C — I can convert C libraries 1:1 to Rust, but C++ libraries hit an impedance mismatch and are really hard to rustify.
Things have a drop-scope. There are hidden initializations. Boxing is required for many things including dynamic dispatch in error handling. The tendency to use FP-like one-liners. All that mixed with funny syntax that hurts C developers eyes.
A lot of "no-go" in C land (at least for me), that makes really hard to learn and to move definitely into Rust (again, at least for me)
I feel like another division is there are programmers who love mathy languages and syntax and those that hate mathy languages and syntax. The former looks down on the latter with contempt. And the latter thinks the former is annoying.
I'm the former kind: I choose C because I like it above all else. [1]
That said, like the author, I'm trying to find an alternative. Well, more like I'm building my alternative because I hate Go. And Rust. And anything else.
I don't know why C fits my brain, but it does. I think it's because my brain is low-level; I like messing with assembler when I get the chance to optimize.
Got it. Thank you for your perspective. I can appreciate that comptime is tricky. Indeed, async zig is hard and annoying (but you're wrong about the function coloring... Here is someone's project where they call an async function alternately from a sync or async context with zero lines of code:
He claims that you can call functions fine in either mode if the functions are known at compile time. I never disputed that.
If he tried to call those functions with possibly unknowable function pointers at runtime, I doubt it would work as well.
There is a difference between compile time and runtime. At compile time, Zig can transform known functions to async to allow turn to call async functions. At runtime, that's not possible.
sure, a sync function and an async function get compiled to two different things under the hood, but IMO colored/uncolored is about ux, not what happens under the hood. In go, or erlang, which don't have async coloring, you don't actually care what the system does under the hood. More generically, if a system takes a private function and can choose to inline it, you probably wouldn't moan that under the hood it's doing something different and thus "inlining is function coloring". Maybe you would.
I guess the question hinges on "just how hard" counts as "hard" for #3. I would say "not having to write an event loop because the compiler generates another version for the sync-calling-async with a single keyword" case counts as "not hard", but this is a matter of perspective. The language design still has rough edges, and you certainly ran into them, but with the function pointer stuff, I think you were really trying hard to break it to prove a specific point (this is a good thing! it will allow the team to fix it). To that point, they simply hadn't run into the specific runtime/comptime thing that you ran into, but it's pretty easy to see how exactly what you ran into can be addressed and fixed at comptime, it... just hasn't yet.
It can't be addressed at compile time. That's the whole point of the uncomputability of the Halting Problem.
The Zig compiler would have to have perfect knowledge of all functions that can be called anywhere. At compile time. Alan Turing proved that this is impossible.
Ok? The universe of all code that can be created by the zig compiler is strictly bounded for any given project at any given time, the halting problem does not apply. At worst, you can do an exhaustive search. The context that you're running the code in can ultimately be known, and the pointer can be set to the appropriate version based on calling context.
"pre-built library files" are not a thing in zig right now, unless you use the c abi, which doesn't have async?
Even supposing zig had prebuilding someday, you would probably have to extern it using async directly declared in the callconv (.async is an option in callconv) and so yes, the compiler would know. It's just that currently zig has a lot of "let the compiler figure out". Maybe that's what's making you uncomfortable?
I'm pretty sure the zig compiler is finite and deterministic, so sophistry around the halting problem doesn't apply.
The Zig compiler is finite and deterministic, yes, but that's not the problem.
The problem is that user code is not finite nor deterministic.
Even worse, Zig's compiler may be deterministic and finite, but the language is not, by far. In fact, the language is one of the most infinite languages in existence because just the type system is uncomputable! [1]
(In this context, the word "undecidable" used by that source means "uncomputable".)
If the type system is uncomputable, then the type system will never be able to resolve all uses of function pointers everywhere.
The whole point of the Halting Problem is that you could have a finite and deterministic Turing Machine that runs other Turing Machines, and that finite and deterministic Turing Machine could run into situation where it never halts.
I never say this when people ask (because most people don't understand), but the real reason I don't like Zig is because of how infinite the language is, starting with its uncomputable type system.
This makes Zig one of the most unreasonable [2] programming languages in existence.
If you don't believe me, ask the Zig team how they plan to solve this problem. Don't ask them if they plan to solve it; ask them how.
Then if they reply, email me [3] with their proposed solution, and I will destroy it. I will do that no matter how many solutions they come up with because math says I will always win.
But if they say they can't, will you believe them?
> If the type system is uncomputable, then the type system will never be able to resolve all uses of function pointers everywhere.
Can you elaborate on what you mean here, and the problems this might cause? Do you mean that some function pointers cannot be resolved to concrete functions? Or that the process of evaluating comptime may be infinite? Or some other problem where the compiler can't determine whether a function pointer is needed for a given function?
Turing-completeness and the uncomputability thereof are widespread, easy to build, hard to keep away, and affects everything that might happen at runtime.
A list of things that can happen is infinite, but that list absolutely includes:
* Whether a particular function pointer resolves to certain functions,
* Whether evaluating the comptime portion of a Zig program will take forever,
* Whether the compiler fails to exclude a particular function from being used as a function pointer at a particular place.
Turing-completeness is a wildly powerful property, but that power is not free; the inability to figure out what might happen in a program is only just the biggest and most apparent cost.
The problems this might cause can be summarized like this: you can never know what a program will do for a given set of inputs until you run the program on those inputs, and even then, you might never know if the program never halts.
You don't need an infinite list of functions to run into the problems with Turing-completeness. You only need effectively infinite behavior, which means all you need is effectively infinite possible inputs.
> If you don't believe me, ask the Zig team how they plan to solve this problem. Don't ask them if they plan to solve it; ask them how.
The Zig compiler has a counter that describes the maximum allowed amount of branching that the compiler can do while evaluating comptime code. The counter can be manually set to a higher value but all compilations will eventually fail if your comptime logic is too loopy. In practice the guideline is to not overuse comptime and instead create a dedicated build step for things like creating pre-computed value tables.
> Then if they reply, email me [3] with their proposed solution, and I will destroy it. I will do that no matter how many solutions they come up with because math says I will always win.
> The Zig compiler has a counter that describes the maximum allowed amount of branching that the compiler can do while evaluating comptime code.
Okay, here's a solution. It's already implemented. Nice.
Now let me destroy it.
You said those compilations "fail"; I presume there's an error that kicks in.
In that case, then some valid Zig programs will be rejected, but your type system is now not technically Turing-complete. It's also less powerful; there are going to be things that Zig's type system cannot express.
But wait; that's a user-defined setting? This means that either users will increase that setting when they need to and run into the same problem, or follow your guideline:
> In practice the guideline is to not overuse comptime and instead create a dedicated build step for things like creating pre-computed value tables.
It sounds like a dedicated build step would be the better option than comptime, to be honest.
Also, if you step out to dedicated build steps, then the compiler no longer has complete visibility into that code when analyzing. At that point, the Halting Problem applies again.
Also, what do you do if a user needs to pass a function pointer to a C function? I presume they must use a sync function? If so, that seems like a direct application of the function colors definition; some functions are unusable in certain places. If not, I'd be interested in what Zig does there.
> In that case, then some valid Zig programs will be rejected, but your type system is now not technically Turing-complete. It's also less powerful; there are going to be things that Zig's type system cannot express.
I think you're a bit too high on all this compsci theory stuff.
All computations in all languages (including ones generally considered to be turing complete) fail when you run out of resources like RAM or patience. Zig introduces an artificial quantity that can be used as a machine-independent way to ensure all compilations eventually end in either success or failure. It's like a timeout, but an actual timeout would be inconsistent across different machines (with different performance characteristics).
The actual state of things is much more boring than what your analysis tries to suggest: comptime is used to do some metaprogramming whose practical value is in good part determined by it not being too crazy, so the fact that you can't make a DisprovesCollatzConjecture type is actually fine.
You have found another feature of Zig that's even more mundane than async where you're trying really hard to misunderstand everything to indulge in... "destroying".
I think your own wording betrays the fact that you care about needlessly debating people more than actually understanding the tools in front of you in order to produce better software.
Comptime is powerful, but without that counter, it's effectively uncontained.
With that counter, it's contained, but then some comptime stuff needs to be separate, where users lose the benefits of Zig's all-at-once compilation model, a model that is necessary because of the feature of hiding function colors.
If you think I care about "needlessly debating people more than actually understanding the tools in front of me," let me dispel that notion.
I was once a fan of Zig.
Yep. I was. I thought it was great and was even going to rewrite all of my monorepo in it.
And then I started studying it in detail, trying to understand it. It took a while, but I came to realize that Zig appears good and has flashy ideas, but that Andrew just didn't understand the consequences of his design decisions.
In other words, I spent time trying to understand Zig before I ever got these opinions.
To go back to the topic, sure the use of this may appear boring, but if tools have a hard time working with Zig code, so will people. That's my problem with it.
You are absolutely right that this is a "boring" part of Zig, but that doesn't mean it's good. It means that that's where complexity hides.
Once I figured that out, I realized that to make better software, it would be better for me to stick with C.
My wording, by the way, comes from a bit of exasperation because to me, it feels like you, Andrew, and the rest of the Zig team are reluctant to accept criticism or that Zig isn't perfect or even the best.
A program is defined to be invalid when it runs out of counters. Problem solved.
>Also, if you step out to dedicated build steps, then the compiler no longer has complete visibility into that code when analyzing.
You can't really "generate code" in these dedicated build steps. Kristoff is talking about generating data. you could maybe build an object file, but then it's bound in using c abi, statically, which we've already solved in this conversation.
>Also, what do you do if a user needs to pass a function pointer to a C function? I presume they must use a sync function? If so, that seems like a direct application of the function colors definition;
In the long run for zig, calling out to c abi, much less one which takes a function pointer, much less one which you happen to have as async, is going to be a rare enough operation that I think people aren't clamoring to go and use it, which fails one of the criteria for your function coloring list. Besides, you just quickly wrap it in noasync context and you're done (you can't pass a zig callconv function directly to a c abi callconv site without at least a tiny wrapper anyways).
> A program is defined to be invalid when it runs out of counters. Problem solved.
Except that the counter can be adjusted by users.
> You can't really "generate code" in these dedicated build steps. Kristoff is talking about generating data. you could maybe build an object file, but then it's bound in using c abi, statically, which we've already solved in this conversation.
So any code generated outside has to be sync? That's exactly what I thought.
But even if Zig only allowed people to generate only data, this is a pretty big limitation.
This may not be a problem now, but it does mean that as Zig programs grow, people will run into this problem. I suspect that this means that Zig will struggle more than C to scale, maybe even more than C++ or Rust.
> In the long run for zig, calling out to c abi, much less one which takes a function pointer, much less one which you happen to have as async, is going to be a rare enough operation that I think people aren't clamoring to go and use it
This assumes that people won't want to interface with software that isn't in Zig. This is another way that Zig will struggle to scale.
> Besides, you just quickly wrap it in noasync context and you're done
This sounds exactly like a special calling technique mentioned in the function colors post.
> (you can't pass a zig callconv function directly to a c abi callconv site without at least a tiny wrapper anyways).
And this also sounds exactly like a special calling technique.
> Except that the counter can be adjusted by users.
So? You've changed the program, and now it's valid. (as my handle suggests) I was a math major: arbitrarily large but finite is a different category than infinite, and that makes all the difference in the world.
> his sounds exactly like a special calling technique mentioned in the function colors post.
No, but the seam between async and sync in most other implementations requires writing an entire event loop. In this case it's often a single keyword, six characters. At worst, you write an extra function header to wrap it.
It's not the extraness that matters, it's the pain of doing so. Hell, even go requires you write "go" in front of your function to run it asynchronously, and I don't think that this counts as "colored functions".
it's not possible for human beings to write correct C code, measured over time
this is not a controversial statement, it's the clear conclusion from any evaluation of available evidence
it's fine that you like messing with assembler, but you can't do that safely -- if the programs you write don't need to be correct then carry on, but if they do need to be correct, then you have a professional obligation to use a higher-level language, with stronger guarantees
edit:
> This brings me to my next reason: I have the discipline to write C at a high level.
factually, you do not. nobody does. you think you do, until you don't. human cognition is insufficient to satisfy this requirement. "discipline" does not fix the problem.
> it's not possible for human beings to write correct C code, measured over time
I don't disagree [1], but remember that Rust can be unsafe too. Async is not a panacea, and it's confusing. And the `unsafe` escape hatch is still unsafe.
> you have a professional obligation to use a higher-level language, with stronger guarantees
Oh? So we have professional obligations now? For FOSS? News to me.
I don't get paid for my work. Not yet. So I have no obligation.
And when I do get paid, my code will be in my own safe language.
This accusatory response is exactly the kind of thing that puts a lot of people off Rust.
> factually, you do not. nobody does.
When I say "write C at a high level," I don't mean that I'm perfect. I mean that I write excellent C compared to all C programmers out there.
Engineering is not about perfection; it never was. It's about doing the best we can with the tools that we have.
I'm an experienced C developer and I wouldn't say this. I'm pretty meticulous, I use as much tooling as I can, and I've still written over/underflows, memory corruption, threading heisenbugs, double frees, NULL derefs, etc. etc. etc. My defect rate has gone way down over the years, but that's probably 50/50 experience vs. an attenuation of my ambitions.
I think the proof is in the pudding here. I can't imagine using a messaging client or an email client written in C, and also there isn't much that is written in C anymore even if I wanted to: you're looking at Swift/Java/Kotlin/TypeScript/JavaScript/C#/Vala/Dart/Rust/Go, and maybe C++.
Is anyone writing production servers in C these days? It feels like they're 100% Go and Rust.
There are notable exceptions, in particular I'm thinking about OpenBSD. I think OS dev is a pretty niche area though, especially when you're talking about OpenBSD's wide platform support, so I think it's more the exception that proves the rule than anything.
Oh wow, someone should alert the Linux kernel maintainers. Do you want to tell them that it’s impossible to write correct C code? And the rust compiler team, too. After all, if nobody can write safe assembly then whatever they’re doing is either unsafe or magically gets the computer to understand rust directly. Or are they relying on LLVM for their code generation? I forget what language that’s written in, but nothing “unsafe” happens there surely.
All code is machine code at bottom. Including the code that maintains abstractions convincing enough for you to think the “memory-safety” of rust or any other language is a static and guaranteed thing and not something that needs “unsafe” scaffolding to support it.
The Rust compiler developers are well aware that it's not possible to write 100% correct code at scale, not least for their dependencies, LLVM provides Rust with ICE (cases where your program crashes the compiler) and with soundness holes where the LLVM behaviour is definitely wrong but it's arguably exactly why and so until LLVM developers decide why it's wrong and fix it, Rust has to either work around that or accept sub-par results.
Much of the Rust compiler is written in Rust and so has fewer problems.
If the existence of C cves in the kernel proves that it is impossible to write correct C, then by the same token any cves in any rust code prove the same thing about rust. This is such a lazy way of arguing. Say something about why the tradeoffs favor a more restrictive and less performant language or don’t, but don’t dismiss the work of many thousands of C developers that runs most enterprise systems with a knowing wave of the hand - it’s not serious.
So by reducing in 70% the costs of fixing the errors caused by those thousands of experts, as validated by the above reports, there is already a considerable reduction in software development expenses.
Lets see how serious those developers get to be around security issues, when liability finally takes off.
I appreciate that you are making an argument and will respond when I get a chance later in the day and after I read a few of the links - I’m familiar with like half of them.
it is not anyone's claim that the existence of C cves means it is impossible to write correct C
the point that is being made (obliquely) is that manual memory management is not possible for human beings to do effectively at scale
at scale means across a broad demographic of programmers, working on a broad set of programs
if you wanted performant and portable then C was all you had for a long time, and there is a lot of good software written in C, but that does not mean that it is a good tool in perpetuity
strcpy and pointer arithmetic are basically not possible to get right
we need to move beyond this very low level of abstraction
> it's not possible for human beings to write correct C code
It's not possible for human beings to write correct code.
The hardest bug I ever found in a C program, one that took cumulative weeks of work until a tractable reproduction was found, came down to a ‘<’ that should have been ‘<=’. No language would have stopped that.
Yes, different languages have different levels of expressiveness, and can preclude or expose different kinds of errors. You'll never have a stray pointer in Python, and you'll never OOM making an accidental copy in C.
I assume this was a logic error, that is, the code as written was a valid C program that didn't do what you meant.
In that case this could have been caught with more careful unit tests, which would have exercised the erroneous condition, and potentially with fuzzing to excite corner cases. C is... not great for either of these.
Nope, when I write tests in my Rust the out of box cargo test will check those tests pass.
If I wrote inline Markdown documentation for part of the Rust code with an example, cargo test will try to build my example, and check it works - not much good having a purported "example" which doesn't even work, is it? And yet many languages don't do this.
Mere existence of a test runner is infinitesimal compared to your proposed careful testing. Saying this as someone who wrote my own test runner, it's a few lines of code.
What you mean to say is you have no shot at correct C, and so the language scares you, and you believe since you can't do it nobody does.
It's true that the community at large writes lots of bugs. But that doesn't mean some people aren't productive with the language and write a relatively low number of bugs. There are some projects with better track records at this than others.
Your opinion is common here. However, you can't just say your opinion is the consensus and that makes people who have other experiences wrong, end of debate. Popularity doesn't indicate truth. I'm sorry you haven't seen it working well, but there are counterexamples where people are productive with a relatively low number of issues.
Inb4 you move the goalposts for "at scale". This is an operating system with capability-based access control which is not something that most operating systems even have.
Also the cryptographic constraints are part of the proof system.
And yet C code runs the world. You might be right in theory but in practice C is the most successful programming language in history. At work we routinely deploy million+ lines of C in production running large international airlines and airports. And it works.
> it's not possible for human beings to write correct C code, measured over time
I agree that C the programming language does not help you avoid memory errors, however, saying it's impossible is too binary. People do make mistakes, whether it's copy editors missing a typo or professional chefs misremembering whether they stocked an ingredient. Projects written in C need longer periods of code review and testing, but they can be made right. The code that took us to the moon was written in assembly after all.
I am a C programmer. For me the language is a means to an end. I prefer to work on problems where both performance and code size is paramount. The kinds of problems where it makes economic sense to spend months or years of engineer time designing and implementing a solution that will compile to a 50kB binary.
I think primarily about the kind of machine code I want to generate, and pick a language that best approximates that ideal output code while still allowing the source code to be as clear as possible.
From these constraints I keep coming back to C. Zig is the most appealing contender in terms of next-gen languages.
If I could get safety without giving up anything, of course I would take it. But I am increasingly convinced that this is not generally possible.
I'm curious what you think about Ada; from what I remember, its memory management facilities have this basically as a first-class thing directly in the language.
I'm old enough to be in both camps. After years of toodling around in a variety of BASIC languages, I took a class on C. For the first time, I went from a vague text-to-result association to a clear picture of machine and its inner workings*. I then learned C++ and Java which were both hot garbage at the time, and a variety of scripting languages.
The scripting langues are great for high productivity and mediocre performance. Shell languages are great for interacting with the operating system. Query languages are great for interacting with big data. But for years, C was the unparalleled language for getting the best performance out of CPU-intensive algorithms. I've still never found a language that erases these boundaries -- while I'm a "C programmer" I'm multilingual by necessity.
Today? C's performance is matched by C++, and more tedious to write. Zig is there on performance, but for my purposes, the language gets in the way. Rust also gets in the way, and can end up forcing reference-counting which tanks performance. I do like how Go mostly gets out of the way, but again, I cannot abide unnecessary reference counting.
* and, yes, the abstraction is a lie, but it's an incredibly useful and high-performance abstraction
> Zig is there on performance, but for my purposes, the language gets in the way
I guess zig for me is indeed a slightly more complicated language than c, and it's way more verbose, but what I appreciate about it is that straightforward code looks good and code that is on shakier ground looks hairier which is basically a "this code is sus, plz review the fuck out of it" sign.
In the end though, what gets in the way for me with C is everything else to ship a "real c program", especially when you want/need someone else's code. Lexical macros, header/code separation, flat namespace, errno, architecture dependent definitions for int, char, etc, having to think about object (file) units, make, automake, config...
At that point the cognitive burden is enough that a small added complexity in the language is worth it
Go uses a tracing garbage collector. Although I suppose that's even worse from this perspective.
If you don't mind me asking, how does Zig get in the way? My own gut feel is that it's the only real "better C" out of the present crop of alternatives.
Carmack said low level programming is good for the soul, he's right. C is a fun programming language, don't even know why. Freestanding C with no standard library is the most fun I've had programming ever.
I'd like to share my project as well: a Lisp interpreter written in freestanding C.
I would argue that Forth is really the perfect thing for such zen-like pursuits, building it up from scratch bit by bit. Freestanding, especially on some more exotic platform, is more fun, of course.
Lol, yeah, the libdispatch runtime is probably about as big as the Go runtime!
I think this has a slight advantage in that it uses a memory arena for a per-request bump allocator so it should keep overall memory usage lower.
The slowest part of all this has to do with Block_copy and the places where I’m very much treating blocks/closures in an OOP type manner.
This could be fixed by writing a different function that did basically the same thing as Block_copy but for all the “methods” on an “object” in a single pass. But who has time for that? :D
Here’s more about those performance issues and some example code that show how slow the approach is:
Wow, really? The project has 25k github stars and 300 contributors. They are on v2.44. Why is it not serious? What makes any software "serious" to you?
In a word, simplicity. A language designed by a single person. I also did program a lot in CLisp, a language designed by committee, with more than 100 different flow control directives(and people often create their own).
I loved and love Lisp as a concept and tool, but hated CLisp complex design so badly. I programmed in Arc for a while because of that.
I have created my own C compiler and interpreter. Something impossible to do for a single person in C++(C++ is way more complicated).
I also love manual memory handling in C because it lets me create my own automatic local systems much more efficient than someone else's "one size fits all" aproach.
I have also programmed mac apps in Objective C and Swift, C++, java, javascript, Perl.
I program today mainly in four languages: C/C++, Python, Rust and Clojure.
With C it is so easy to create python modules. It is trivial to interoperate from C++ with C. Clojure is a Lisp that is simple, clean design, and you could interoperate with java objects. Rust has some advantages over C, but also disadvantages(it is too bloated).
This is why I've kinda built my own libc on the bare minimum that the system gives me. The POSIX and Windows API's are garbage; C the language is great.
i have written a fair bit of code using the windows and posix APIs, and they are OK - what features of these, often written by talented programmers, do you despise so much?
To pick the same example from both, I hate both fork() and CreateProcess().
Microsoft wrote "A fork() in the Road" [1] describing the problems with fork(), and they are right: fork() is too simple, it doesn't scale, it is inefficient, and error handling becomes next to impossible.
(In my code, I have the child process return exit codes from 255 on down for error handling. It assumes, probably wrongly but right enough, that most programs won't use those exit codes.)
But CreateProcess() has the exact opposite problem: it's too complex and limited because it takes a large, fixed set of arguments. It takes many lines of code to set up for it, and once you actually start the process, you have no control over it, which sucks if you need to do something with the new process that CreateProcess() cannot do on creation.
Windows does have ways of modifying running processes, which is great and sort of makes up for the limits, but heaven help you if you need to use one of those functions on the new process because you have no control. The only thing you can do is to suspend the start thread of the new process right away, do your thing to it, and resume the thread, praying that the new process hadn't created a new runaway thread before it was suspended.
(And all of that is not even mentioning that the Windows way of passing a command-line is to pass a string to be parsed, not a list of strings. My code has a function literally to take a list of strings and turn it into a Windows-compatible command-line string with all of the juicy backslashing that implies.)
The right API is in the middle: a zero-argument function to create a new, blank process (not a copy of the current process), but to create it in a suspended state, so you know it's not going to run away from you.
Then, you use Windows-style functions to change the process, before it even starts, to set it up. Once you're done, unsuspend it.
That end result gives you as much power as fork(), with the better scalability of CreateProcess(), and better ease-of-use than both. You could even have functions to map in a copy of the current process if you so wish, to implement fork() for process checkpointing!
In other words, talented programmers can implement things and make them work, yes, but it doesn't imply that they are good at design.
> I no longer want to have to think about memory management and related issues.
In some cases efficient memory management _is_ the problem to be solved. I'm not just talking about esoteric or embedded systems here. If you're developing for modern GPU's with a modern API, like Vulkan, you must manage GPU memory yourself. With direct memory access you can do many "unsafe" things, like memory aliasing, to improve performance. "Safe" languages, like Rust, will not help you here.
Tense is very important with this article. People who chose C. Whatever these reasons were that made someone chose C, that choice may have been made 20 years ago, and the choice may pan out very different if made now. But now you have 20 years of C experience…
I was not there in those days (somewhat young). I have still chosen C, and I chose it before I had experience with it. I just like the low-level nature.
I used C for my own just in time compiler because I wanted to learn C.
I still don't "know" C or what's a customary way of doing things because I only started with C code by Martin Jacob that executes machine code in a memory buffer ( https://gist.github.com/martinjacobd/3ee56f3c7b7ce621034ec3e... ) and some Perl Compatible Regular Expression library example C file.
I want to know C enough that I can create good APIs in it and transpile to C effectively because I want to take advantage of LLVM and gcc optimising backends.
It's interesting that C has managed to remain relatively stable whilst everything else has ballooned in size. I spend most of my time writing C# and it has become quite large. I wouldn't want to pick it up from scratch now.
I am definitely the former. I didn't like C too much in 1986 but there wasn't really anything else except writing in Assembler! Having programmed in Pascal, it wasn't too much of a leap to transition to C. The C compilers at the time were buggy. 37 years later, I still write C programs. I won't touch C++.
I consider myself the latter who just hasn’t had the time to look into Rust or Zig extensively. But also, I work in safety critical application and it would be hard to shift the organization over to Rust for a lot of reasons, both technological and political.
I think there are way more types, or subtypes. The first category can be broken down into "people who like C because it's simple" and "people who use C because they need to do manual memory management" (and that can be broken down into "has not tried Rust" and "has tried Rust and didn't like it"). The second category can be broken down into "people who wrote a program in the 1980s in C and are now stuck with it" and "people who learned C in the 1980s and haven't tried to learn a new language" and "people who learned C in the 1980s-1990s and think that Python and Ruby would be too slow for their purposes (no longer true)".
I learnt C in college but C++ was emerging as the language of choice circa 1995 when I got into my first job.
This was before C++ was standardized and we had to make do with whatever MSVC 1.5 would do. It did not even support templates back then and even the highly polarizing STL did not exist yet -- in a cross platform way; The STL source itself was available from 1994, and if I recall right, the C++ compiler on Sun/Solaris supported templates etc., We were then a Sun/Solaris + Dell/WinNT shop then and had software running on both and therefore settled on the lowest common denominator of features supported by compilers on both platforms.
Heady days.
Given that environment, I was comfortable switching between C and C++.
Borland did templates since 1993, Borland C++ 2.0 and Turbo C++ had early support for them, and BIDS 2.0 changed from pre-processor macros into the templates experimental design.
MSVC was always behind until Borland management messed up.
Personally, the primary reason I'm a C programmer because the main project I work on is a million lines of code, and it's C... As with COBOL, legacy code will mean there's still demand for C programmers for a fair while yet.
I also mostly use other languages these days, but there's still something about C that always feels like coming home to me. Maybe it's because it's what I used during my apprenticeship which is where the magic of programming finally clicked for me (after a failed attempt taking a programming class). Maybe it's just the simplicity and lack of rules to learn.
I don't see how simplicity is lost through use of pointers. Dynamic language variable references are essentially pointers and they couldn't be simpler.
There is (or maybe was a few decades ago) a third type. A person who was taught C programming in the school/university, because back then it was the norm to teach it and nothing else, and who does not have enough motivation or abilities to learn anything else on their own, even if C is clearly the wrong tool for what they are doing.
With C, programmers of different types (there are certainly more than two) can be combined into one operational unit by a clever manager.
Sometimes differently typed programmers will implicitly agree to adopt a uniform approach before working together on a project. Sometimes the programmers won't do this by themselves and so a manager is needed to explicitly guide them to adopt the appropriate type.
However, some types stubbornly resist conversion, and so are tricky to work with. Pointy-headed programmers tend to be surprisingly flexible, and the ones who stare off into the void are often the most useful when you need them to function effectively. Overall, managers who can understand and utilize this hierarchy of programmer types in their organization may find themselves getting promoted regularly.
Of course, many people can't stand these corporate structures and prefer the simple farmer's life, raising ducks instead. Quack!
I switch between both depending on my mood and the particular application.
For me: Rust killed C++ but not C
I have an on-again off-again love affair with C. And in its domain it’s still unmatched: low-level close to hardware where you directly need to deal with concepts that are normal abstracted away: alignment, custom layouts, type punning, pointer tagging, different types of memory, cache coherency, DMA, kernel bypass, etc etc
Sure you can do it in rust unsafe but it’s (intentionally) a world of pain.
But, when you start needing bigger abstractions or you work with a team larger than say 5, C falls on its face super fast.
But it Feels Oh So Nice otherwise.
Here’s to me hoping Rust unsafe becomes more usable and can be a proper C successor someday. But.. I’m happy to be rid of C++ :-)
(I’ve been meaning to write a blog post on this for some time)
Many people I know (myself included) started in the second category in the 1980s and 1990s (C = God's programming language) and then moved to the first category once attractive alternatives (e.g., Go) that met our use cases became widely used.
> They feel strongly drawn to C's virtues, often explicitly in contrast to other languages
Can someone point me to which set of virtues you are talking about here? Simplicity? Value semantics and imperatives, little abstractions over what the computer OS is actually doing? Manual control over almost everything being done? Are such "virtues" safe and dignifying under an universal context of computer programming? Isn't it a bit dangerous to let such Virtues take over in modern programming dealing with heavy abstraction layers because of the yield in complexity of modern hardware?
I used C in the late 80s and 90s because it was the only viable option for many use cases. In the late 90s I got heavily involved in Java, and C usage dropped. Had some spikes of C++ work (which I despised), and then working in many different languages with no real work on C or C++ in at least 18 years or so.
What always killed me the most in C was the absolutely anemic standard library. OS’ had more libraries, but still very low level stuff.
To be a bit pedantic, those options aren't mutually exclusive. An appealing property of C is that it's easy to target almost any device (often making it the best or only option). So it's possible to like C because you like (and need) something you can compile in lots of different places.
Implicit in the article is that there are only a handful of languages capable of certain use cases. Eg, embedded, bare-metal, programming operating systems, performance-sensitive code etc. So, your options are limited (C, C++, Rust, Zig, ADA, maybe a few more?), which would drive option 2.
What about those who are stuck with it because of the huge legacy code base? They wouldn't choose it deliberately for anything new, but there isn't much they can easily do about it in the above case.
People who chose C as the best option at the time might not be even around the project anymore.
> In the unary system, the number 0 (zero) is represented by the empty string, that is, the absence of a symbol. Numbers 1, 2, 3, 4, 5, 6, ... are represented in unary as 1, 11, 111, 1111, 11111, 111111, ...
Obviously there is C++ which can leverage most of this too, but C++ traps you into an ABI which is difficult to use from any language which is not C++.