224 comments

[ 2.9 ms ] story [ 240 ms ] thread
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 am the first sort. I mostly like the ideas from C but don’t like the actual implementation. Which is why I’ve moved on to Zig.

The author asserting the second sort would have moved on to Rust makes sense. They are ones who are trying to get away from C ideas

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.
[flagged]
… and those who mistake it for trinary.
10 in binary _is_ 2 in decimal. I think you've made an off-by-one error in your critique.
In fact, 10 in any base represents the base number.
But it's (decimal) 3 in ternary, I think that's the point (you get a 3rd group of people if you're mistaken about the base)
Could be that I missed the joke.
And those who know that "10" can be any number if you change the base.
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++.

Good points. I like C, I think simplicity and hardware alignment make it very useful. Simplicity wins in the end.
C is not aligned with modern hardware and optimizing compilers are severely hampered by things like undeclared pointer aliasing.
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.
It is called restrict and usually a fun problem to debug if one causes UB by actually having two restrict pointers doing aliasing.
> 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.

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.

Out of curiosity, have you looked at the ASM inlining options in Rust? https://doc.rust-lang.org/reference/inline-assembly.html
Rust's inline assembly and configurable targets sets the standard going forward.

I just wish their trademark policy relaxed some of the non-trademark related requirements making it a deal-breaker.

What does the trademark policy has to do with this?

Btw the recent fuzz recently was about some proposal and not the actual one. This is the policy: https://foundation.rust-lang.org/policies/logo-policy-and-me... and I don't see any deal breaker.

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.
Unless that ABI is something like COM or WinRT.
We have entire OSes built in rust, so that' not the case there. That's both for mcus and desktop machines and beyond.
D has a very nice inline assembler, bitfields, inline control, alignment control, etc.

C's "register" keyword has been ignored since 1990 or so.

"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:

    import core.stdc.stdio;
    extern (C) void main() {
        printf("Hello world\n");
    }
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.

One big feature is D has modules. There's no reason C can't have modules, but they don't. You don't need to code .h files anymore.
LDC and GDC both have the section concept exposed as a UDA.
GNU jitter right? I’ve seen (a few?) highly detailed slide decks about it. Extremely cool stuff

I found this pdf but I thought there was a different one, anyone link to that?

https://binary-tools.net/jitter-binary-tools-summit.pdf

Not jitter, but I'm familiar with it and have used some of its ideas. (More Jitter slides referenced on Luca Saiu's page:https://ageinghacker.net/talks/#jitter-talks)

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.

Linux is reportedly written in C and it still has a good share of plain assembler.
> 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

(comment deleted)
we don't have standard ABIs for either C or C++; we have conventions.
Counterpoint:

https://wiki.osdev.org/System_V_ABI

The System V ABI is closer to being a standard than some official standards are.

C++ gets odd with name mangling, but the platform's C ABI is typically the tune every other language must dance to for its FFI.

> 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

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.
the simplicity of c is an illusion the moment you start using pointers and define macros. nothing is harder to understand than a big C program, imo
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.
> Maybe it's just the simplicity and lack of rules to learn.

This is how I feel about assembler. No rules, you on your own. And it is impossible to imagine simpler language. Though I do not write asm lately.

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.
actually, C has ballooned in size and complexity. it has come a long way from the first edition of k&r.
At the same time, people have expectations for ability now.

A beginner might want to make a web request, good luck doing anything beyond mimicry with C from code samples if they get that far.

With python, they could pull it off quickly.

C23 versus K&R C, and then there are all those compiler specific extensions.
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.
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.

Yes, that’s right. In fact, in college we used Borland compilers.

But my first job had standardized on MSVC. So, that determined what I used on the job.

Loved Turbo Pascal and Turbo C++ IDEs in those days. Simple and fast!

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.

https://en.wikipedia.org/wiki/Alef_(programming_language)

http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/

http://doc.cat-v.org/inferno/4th_edition/limbo_language/

https://www.bell-labs.com/usr/dmr/www/chist.html

https://www.bell-labs.com/usr/dmr/www/vararray.pdf

> yet they miss that for Plan 9 and Inferno, they also decided to go with automatic memory management languages

And which of these three operating systems won?

Operating systems don't "win" because they make the best technical choices ... or even because they are best for their users.
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.
The winning characteristic in this case is "it was already there, and it was good enough". This is a recurring theme in the industry.
The free beer one, because everyone likes to get source tapes for free, regardless of the quality, free usually wins out.
This doesn't seem to hold in the general case. Windows and Macos are not free in any sense, but vastly outnumber linux on the desktop.
The desktop is not the general case.

By install count, Linux runs on far more platforms than anything else: smartphones, tablets, servers, virtual machines, embedded devices, supercomputers, and spacecraft.

Because Linux folks cannot get their act together in what means a full stack desktop experience.

Not even Android games get ported in any significant number to GNU/Linux.

The war of Linux Distributions is ten fold worse as the UNIX wars.

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.
"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."

Source: https://www.catb.org/esr/writings/taoup/html/plan9.html

(comment deleted)
I appreciate the sentiment, but:

- 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 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.

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.
But its not really.

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 deceptively simpler.

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 think you might not have read my bullet point that mentioned rust very carefully.
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.

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.

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!

C with libdispatch and clang blocks is the most fun I’ve had programming in quite some time!

Here’s a web framework (complete with ORM) modeled on ExpressJS written in C:

https://github.com/williamcotton/express-c

The finished product is < 90Mb Docker image that idles at like 2Mb of memory.

There’s also a lot of examples of the (basically required) support tooling like Valgrind, AdSan, etc.

Check it out!

It looks cool. The DB library is appreciated. However, you would get about the same idle memory and docker size with Go's Fibers framework :)
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:

https://github.com/williamcotton/express-c/tree/master/resea...

go-fiber is a weird, non-idiomatic, and non-serious project, fine for a proof of concept, but definitely not something anyone should be using in prod

but your point is sound, any reasonable go http server will have the same level of memory usage at idle

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?
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.

https://github.com/lone-lang/lone

Ooh, I really like this and from a cursory glance it looks very legible, I’m definitely going to poke around later!
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.
this project makes assumptions about received input (specifically encoding) which aren't guaranteed

fine for a toy project, not something that can be used in anger

Are you referring to the req->sendf as seen in the README?

Yeah, I definitely wouldn’t use that approach for user input! There’s also support for mustache templates and JSON-API endpoints as well.

Or are you referring to something else?

But yes, please don’t use this for anything serious!

“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 you are the first type, you likely get the second type’s taste wrong. Rust is very very different from C both in terms of syntax and philosophy.

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.

The use of macros and compiler plugins is pretty much closer to C++.
> "low-level control, not OOP, not C++".

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.
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.
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 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.
Why I love C?

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).

> impossible to do for a single person in C++(C++ is way more complicated)

I wrote a fully compliant C++98 compiler (Digital Mars C++).

>I have created my own C compiler and interpreter.

It's relatively simple because everything is UB/UsB/IDB. ;)

> In a word, simplicity.

For fun, go to /usr/include and try to understand the system .h files.

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.

[1]: https://www.microsoft.com/en-us/research/uploads/prod/2019/0...

and does your library support threads? in my opinion, MS made the better design decision - first-class support of threads over forking new processes.

but this is why we have different software architectures :-)

My library does support threads. In fact, it has structured concurrency as a theme through the whole codebase, based on OS threads.

I do agree that first-class thread support is better.

The irony is that what you describe is pretty much the traditional OO way of doing things. In C#:

   var process = new Process();
   process.StartInfo.FileName = "foo.exe"
   ...
   process.Start();
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.
(comment deleted)
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)".
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.
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 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.

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.