C Isn't a Programming Language Anymore
gankra.github.io
gankra.github.io
A * B;
Is it A multiplied by B, or B declared as a pointer to A? It can't be resolved without a symbol table. But there's another way that works very well: it's a declaration. The reason is simple. The multiply has no purpose, and so nobody would write that. C doesn't have metaprogramming, so it won't be generating such code as an edge case (unless using the preprocessor for metaprogramming, in which case you deserve what you get).But there's a worse problem:
(A) - B
Is it A minus B, or casting negative B to type A? There's just no way to know without a symbol table. One might think who would write code that has a vacuous set of parentheses around an identifier? It turns out they don't, but they write macros that parenthesize the arguments, and the preprocessed result has those vacuous parentheses.D resolves both issues with:
1. if it parses like a declaration, it's a declaration
2. a cast expression is preceded by the keyword `cast`
and D is easy to parse without a symbol table.
Personally, I wonder how many of these things would go away if pointers were a suffix operator with a different character (maybe the @ sign), and if casts looked like function calls.
A B@; # B is a pointer to type A
A(-B); # A is either a function or a type for casting
# Same AST regardlessC needs forward declarations for some things, and it needs a symbol table to resolve some parts of the grammar. All I was saying was that I think you could resolve both of them with minor changes. (I see that D has a "cast" keyword, and that's obviously one way to do it.)
IIRC that's how Pascal did it, using a caret ^ for denoting a pointer.
A pointer to type A is:
var B: ^A
The parser knows it is a declaration because of the var, and it knows ^A is the type, because of the colon. That it is a caret does not really matter hereBut thanks to Moore's Law and the hedge by the Writh's Law, you have significantly more powerful hardware yet not so faster software, because we started to deploy languages worse than C: C++. C++ with template, is Turing Complete, and this means every template expansion is potentially undecidable, meaning it could run forever, where most compiler turned a blind eye to by putting a "recursion depth". Having templates, even without "recursion depth" alone consumes even more memory than C, and so that's why we complaint about C++ compilers are memory boggling, while C has (relatively) low memory overhead. It's just that times are different and the perception about memory use is not that apparent anymore.
Also, I frankly don’t get why templates are “bad” according to you. They allow for proper metaprogramming not relying on ugly hacks like memory layout conventions, etc.
The grammar of your file depends on previously declared symbols. But which symbols have been declared depends on the header.h files you import. But which headers you import depends on the -I options you give to your compiler. Except there's no standardized way to express what -I options your project uses, and they might change depending on your build profile.
Any modern text editor can give you good syntax highlighting for a Rust file or a Go file basically as soon as it opens. When it opens a C/C++ file, it has to do a lot of guessing.
(This is not conjecture, by the way. I tried to integrate clang's implementation of the Language Server Protocol in Atom for my end-of-studies project, and it was not fun.)
But there's another way that works very well: it's a multiplication. The reason is simple. A declaration of a variable that isn’t used has no purpose, and so nobody would write that.
As I think you know, C doesn’t handle this case by guessing that it must be a declaration. Its lexer looks in the tables that its parser creates to check whether a type called ‘A’ is in scope (https://stackoverflow.com/questions/41331871/how-c-c-parser-...)
A fellow named Bjarne Stroustrup fixed that bug, though. In the plus plus dialect of C, A * B could reboot your system, without any #define macros for A or B.
Lol at this. Love this eloquent style, and pretty much agree. However, C's reign is not absolute. Virgil compiles to tiny native binaries and runs in user space on three different platforms without a lick of C code, and runs on Wasm and the JVM to boot. I've invested 12+ years of my life to bootstrap it from nothing just to show it can be done and that we can at least have a completely different userspace environment. No C ABI considerations over here. It can be as romantic as just you and the kernel in Virgil land. Heh.
Definitely looks like a cool project. Can you write an OS in it, for microcontrollers ?
Another option would be to compile to Wasm and then import GUI-related functions that are then implemented in the Wasm host environment, e.g. JavaScript.
As for microcontrollers, that was in fact, Virgil I, circa 2006. :) My prototype compiler back then generated C code and then you could compile with avr-gcc. Nowadays, I don't have a C backend or native backend for microcontrollers, so it'd require a new backend and a new exe format. Or maybe again, try to compile Wasm to AVR.
That's the real problem the author is ranting about. If you've solved that, too, then I think that the author's article isn't the only Rust team member who'd like to talk to you.
Sounds like that's Rust's problem, or maybe Virgil's? Why is it C's problem?
If Rust is going to "replace C" for systems work, as the more grandiose claims would have it, it's going to have to replace all of C.
It's not like writing a POSIX-like system is that hard. It's a lot of work, sure, but it's been done multiple times, by unpaid volunteers at that.
This complaint basically boils down to "We can't/won't/haven't reimplemented this low-level stuff in our whizzy new language, and that's somehow C's fault".
"Easy"? Depends on your definition of "easy". Doable? Absolutely. But complaining won't get it done.
Of course it's "possible". Of course, complaining about it won't get it done. But I can easily envision an alternate universe with the same OS & CPU architecture variety, that has the effort required for doing this be ten times smaller, and I don't see it being utterly pointless to ponder the viability of that if only just for fun (which the swearing in the article hints to me it is).
I, as an author of a small language implementation, would really prefer to not need to implement more than one C FFI ABI, much less 176. I'd meet somewhere in the middle, but 176? nah.
The compiler provides a mapping from the stable, (relatively) portable, well-defined API -> the unstable, non-portable, ill-defined ABI of the host system.
... and people complain all the time about Rust fanatics talking about rewriting everything in Rust.
They just don't ever do any of them.
- You have to speak the C ABI to talk to the OS -- yes, if you want an OS to support more than one language, or you want multiple languages within a single process, you need a common ABI of some sort. What is the alternative that doesn't involve a common ABI?
- C has too many ABIs -- yes, there are a lot of different kinds of hardware, and sometimes multiple software ecosystems evolved in parallel on the same hardware (eg. Windows and Linux). What is the alternative?
- ABI changes are hard to make in a non-breaking way -- yes they are. It's fundamentally a hard problem. You could compile everything from source every time, but the open-source world seems to have decided that there isn't enough time or CPU power to "live at head" and build from source every time, like Google does: https://abseil.io/about/philosophy#we-recommend-that-you-cho... (Disclosure: I work at Google).
- "You Can’t Actually Parse A C Header" -- this is, IMO, the most actionable objection that the article raises. You could imagine a subset of C that does away with macros, typedefs, etc. in order to be easier to parse, and in doing so forms a more accessible ABI specification language. That seems eminently doable. But parsing is only part of the overall battle: actually implementing the ABI for 176 triples seems like the bigger problem. Especially when you add the ABI-impacting function attributes mentioned in the tweet.
As a C fan, it's hard to see the language itself blamed for what are (in my view) just fundamentally difficult problems.
> You have to speak the C ABI to talk to the OS [...] What is your alternative that doesn't involve a common ABI?
Emphasis is on the _C_ ABI. The alternative is something that at least tries to be designed as a universal ABI.
> there are a lot of different kinds of hardware, and sometimes multiple software ecosystems evolved in parallel on the same hardware
If there was an explicit ABI standard though, evolving ecosystems would have to go through the standardization and thus you just couldn't get conflicting ABIs for the same thing.
If such standardization could cut the number of ABIs in half, it'd still be a huge win. And I think it'd be possible to go much further than that. Furthermore, it may be possible to split orthogonal things apart, such that instead of e.g. 50 targets, you have 5 OS targets and 10 arch targets, each specifying a separate part of the ABI. (current ABIs obviously do this to some extent, but not in any specified & structured way)
> As a C fan, it's hard to see the language itself blamed for what are (in my view) just fundamentally difficult problems.
I also like C. I agree these are hard problems. But I don't think they're problems that C should be dealing with. But we're pretty much just stuck with C.
But there are lots of reasons why projects/companies might want to create their own ABIs, or change existing ones. You seem to be suggesting that if an ABI standards body existed, that all vendors would be following a minimal and unified set of ABI standards. But the very examples given in the article contradict this vision. Arm64 does have an ABI standard (https://developer.arm.com/architectures/system-architectures...), but Apple chooses to deviate from it anyway with the aarch64-apple-* set of targets: https://developer.apple.com/documentation/xcode/writing-arm6...
I don't think this has anything to do with C per se. Whether it was the "C ABI" or the "Standard ABI (totally separate from C)", vendors would have incentives to do their own thing. Especially since this stuff all sits so close to the hardware, and the ABI has a significant impact on performance.
I'd be surprised if a ton of applications in the wild aren't violating Apple's register x18 requirement, especially given that, as far as I can tell from searching, applications can just freely use it on macOS+Apple Silicon.
In my mind, an ABI standard would have some 3 parts - object layout on the heap, foreign function calling convention, and background requirements/standards (though i'm possibly missing some important part). The Apple changes would only affect the latter (assuming function calling has at least one volatile non-argument register, which could be reused for that). And object heap layout has pretty much no business being ever changed. (importantly, the first two would be only for cross-language communication; your language itself could do structures & calls however it likes)
edit: it seems that the ARM ABI itself names r18 "The Platform Register", allowing ABIs to change its meaning, and recommends programs to not use it if at all possible, so Apple's restriction isn't even that unreasonable.
macOS outright zeroed x18 on each context switch for a bit, and it's zeroed on each context switch as part of the Meltdown mitigation for older SoCs in iOS.
On Windows, x18 is used as the thread environment block pointer.
On Android, x18 is not accessible to third party apps as part of the ABI. It is reserved for the shadow call stack.
(tldr: on most OSes x18 is explicitly reserved by the platform)
That's not an either-or. E.g. WinRT is an ABI that is specifically designed as cross-language, but it can be reduced to the C ABI (e.g. vtables are basically structs of function pointers etc).
You don't need to parse C headers in order to use the same ABI. You can read DWARF info which is designed to convey type definitions, function/method prototypes etc. in a machine-readable way. It's already there, no need for a separate "IDL" standard at all.
A start? https://github.com/cil-project/cil
A more reasonable option would be to have a better-defined format to describe the ABI (let's call it the Depandable Wanted Abi Recall Format) and have the C compiler output that DWARF for each header. Then other languages that want to make use of that ABI only need to parse the intermediate format and don't need to care about all the ugly details of the source language.
Much of the content posted on HN nowadays is like this.
C isn’t ideal, but it’s actually not so bad and it could be worse (it could be C++). Yes parsing C is non-trivial, but that’s true of every language. These days you have libclang and a dozen other decent C parsers. Back in my day we had to use a hacked up version of GCC (and we liked it).
Also, C doesn’t have a standard ABI, but every real world platform defines a C ABI. And it’s pretty simple. Meanwhile trying to handle all of the cases of C++ vtables took up weeks of my life (and I ended up shipping without fully supporting multiple inheritance, which is stupid anyway).
The bigger problem for writing FFIs is, in my opinion, memory management. That’s where it gets really hard to paper over for the binding user that you’re talking to C.
Have you heard of our lord and savior LISP?
> My Google SoC project was writing a c++ bindings generator for Common Lisp
Not sure about this. C is ambiguous without a symbol table, and the preprocessor is necessary for populating that. Parsing against all of the correct headers (e.g. libc, kernel headers) with correct macro values is tough.
Most languages' concrete syntax treat type references in an unambiguous way (e.g. separating them from values with `:`), which is just much easier.
Of course it doesn't. C implementations do. This isn't really any different than most other languages, but feels different because C doesn't have a blessed implementation that all other implementations must interact with.
That's a strength. It means C is found on esoteric microcontrollers as well as powerful modern desktops. That wouldn't work as well as it could if the ABI were uniform on all targets and implementations.
And yes, it can act as a protocol. It's the simplest way to access host ABI communication without understanding it.
Microcontroller programs are also pretty small; it's not difficult to load the whole program into memory on a workstation and compile the whole thing together. This is how Virgil (circa 2006) worked.
And I definitely have received .o files from vendors.
On the other hand, `sizeof (int8_t)` is the same size no matter which architecture you are running on.
If you, as the programmer, are depending on the bit-width of an integer type, C has got you covered mostly.
EDIT: See account42's reply below.
ORIGINAL COMMENT: No, it's not. CHAR_BIT is not required to be 8 and sizeof (int8_t) is not required to be 1. Hell, int8_t is not even required to be present in <stdint.h>, but int_least8_t is.
The complaint isn't that C lacks a well defined ABI, it's that e.g. x86_64-unknown-linux-gnu potentially lacks a stable ABI.
That is, in principle the strength of supporting a wide variety of platforms has absolutely nothing whatsoever to do with the quality of implementation on those platforms.
I'm not sure what's the goal in complaining about that. Just venting?
Seriously though what on earth is your point here? This is an incredibly thorough article on an interesting, difficult subject, with a lot of room for different technical choices. Do you just not like the tone?
Except they don't. The OS doesn't have an ABI at all, the specific complier does. That's why there's unique triplets for windows for whether or not you're compiling for msvc or gnu, as in x86_64-pc-windows-gnu vs. x86_64-pc-windows-msvc. Both are x86_64 windows, yet they are different ABI targets.
And even on MSVC windows alone there's not even a single ABI - there's __stdcall and __fastcall which change the ABI on a per function basis, and compiler options that change the default convention ( https://docs.microsoft.com/en-us/cpp/build/reference/gd-gr-g... ). So to figure out how to invoke Windows' "rock solid" ABI you need to not only know the header & compiler used, but also the compiler parameters.
Similarly, again per the article, clang & gcc on x64 Ubuntu 20.04 can't actually call each other reliably. They don't agree on a few things, like __int128 which is actually documented explicitly by the AMD64 SysV ABI!
The fact that the biggest compilers don't quite agree on the ABI for the most popular desktop CPU is not a success of C's portability or flexibility, just a historical quirk caused by the C language moving slower than evolution of the hardware and needs of operating systems.
The very premise of TFA is fundamentally incorrect, and there is no way to reach a valid conclusion from an incorrect premise.
> An operating system has an ABI. Programs compiled from C source use the OS's ABI. There is no "C ABI".
I'm having difficulty getting these two ideas to reconcile.
Linux is unique in that you can hand-write assembly to perform a system call and have it work across OS upgrades. FreeBSD does not have stable syscall numbers: open() must be a libc call.
The OS-provided API for the tasks are libc and Security.framework. Do you have any suggestions for a better API?
Of course, the POSIX-likes never adopted this, whereas Microsoft nowadays has some fourth generation of this IDL stuff (WinMD) that they are also slowly porting all the old C API definitions to (see win32metadata, also used for defining stuff like the Win32 package for Rust).
Also, of course, this all has its own issues too, for one COM's definition of reference counting is a bit picky, and there were a lot of advanced 'implicit RPC' features that were also more inherent footguns, but at least it doesn't involve what is ranted about here.. mostly.
Windows went to substantial lengths to support non-C languages. It’s not obvious that the result was much of an improvement.
> Case Study: MINIDUMP_HANDLE_DATA
I think the author is well aware of Windows.
I work on an implementation of a high-level language (implemented in C) that probably less than 100 people have used, and a C FFI has been asked for plenty of times. You can't get around it.
Would some thing other than C being the ABI be better? Who knows. But the current situation just sucks.
edit: I'd like to explicitly note that I like C as a language. But it still makes for a bad ABI, because it wasn't meant to be one, and barely even works as one.
The C language doesn't define system calls. It doesn't define a lot of things that were either (a) created in C and presented via a C ABI (b) created in something even less universal and presented via a C ABI. Both (a) and (b) were done by people and projects who are not the C language and do not control the C language.
The issue is that there isn't really an alternative. Obviously a cross platform ABI is never going to exist (different endianness, alignment, register usage, stack conventions just considering the CPU, OSs themselves add more complexity). We could have an IDL to describe in details the ABI, but then:
- you need all OS vendors to be on board, which is not going to happen.
- even if they did you can be sure it will be forked in a myriad of dialects and non conforming implementations.
- even if everybody plays ball, bugs will still happen.
The best next is for language designers to come up with community maintained IDLs and tooling to interface with various languages instead of waiting for platform vendors to provide them. This is a realistic solution that can work, but at this point you might just accept that C fulfils this role already even if it is far from ideal. Just embrace libclang and hold your nose.
edit: there is also the option of targeting a single virtualized platform like the JVM or CLR which is great, but not really appropriate for a system language.
The author is clearly and abundantly aware of this, and it isn't her complaint. Did you read the article?
As I read it, the author's thesis is that all programming languages have to talk to the operating system and the operating system is written in C so the operating system uses C calling conventions which leaks C's "ugliness" into the implementation or expression of their beautiful language.
I kind of think of this as the "I like computers but don't really understand computation" fallacy. It is fundamental lack of understanding about the nature of computer architectures and what they can and cannot do vis-a-vis how you might express that in a programming language.
One of my professors in college was fond of saying that "All programming languages are just syntactic sugar around machine code." Which is fundamentally true, and tries to capture that at the end of the day what ever your language "says" has to be expressed in machine code to actually do what it does.
You will spend a lot of time in this space writing a compiler, and code generation is an art all of its own.
But you can side step, a bit, by not writing a compiler, and instead writing an interpreter. The series of articles that were posted here gave a good intro, and while you still have to do the "naughty bit" where you write code in some compilable language that can pretend to be a computer of some different form, you can make everything look like your language.
I always encourage people who are "learning computers" to actually write a compiler (there are some good starting points for that but online courseware from MIT and other sources can get you the lecture material too). Doing that helps broaden one's perspective of what language designers and implementers are up against with regards to pretty much every computer working the "same" way (Von Neumann or Harvard architecture wise)
My position is that this is less of a burden than writing something that goes from the "glorious language" into machine code.
Understanding why that is less of a burden is gained by writing a compiler where you go right from "expressing what you want" to "machine code that does that".
The last thing I want when I write code in assembly is to have to call a C library just to invoke a system function.
In case anybody wants to go down the rabbit hole of shit you should definitely not use in production (but we did anyway), this was my starting point: https://words.filippo.io/rustgo/
I did not, however, `no_std` or avoid heap allocation on the Rust side. Everything worked great, including running a multithreaded tokio runtime.
Still do not recommend in prod ;)
I disagree. Machine code has a small stable surface area. Compiling to a bootable image for a single-tasking machine is easier than compiling to a well-behaved userspace unix program that operates the way users expect.
It's only less of a burden if you're guaranteed to have a C compiler hanging around any time the C ABI breaks. If you aren't guaranteed to have a C compiler hanging around any time the C ABI breaks, then you are probably just as well off generating C-ABI compatible machine code from an ABI description written in "glorious language" and updating it when the C ABI breaks.
Non self-hosting languages distributed as source code can just lean on the C compiler for everything.
It's also quite feasible to target Linux without worrying about the C ABI, as the Linux system call interface is famously stable, but that is not true of any BSD.
Even that is not the real complaint, since the alternative to this (N different FFI-style APIs) is madness.
The heart of the complaint is "great, we have effectively a single interface to almost everything, which is probably a good idea, except that the interface sucks".
Not necessarily. C is how your language interacts with the OS if you use the OS's C library. But not all languages do. Go doesn't.
The only thing you have to do to interact with the OS is make system calls. You can do that without going through the C library. It might be a PITA to do it if you're not working on Go for Google, but if that's the real problem, that's what the author of this article should have complained about. (It's actually less of a PITA on Linux because the syscall interface is implemented using software interrupts. Windows is more of a PITA because the OS goes out of its way to make its syscall interface look like a C interface.)
...on Linux. Pretty much everywhere else, libc is the way to interact with the kernel; even Go goes through libc on ex. Darwin and OpenBSD, because Linux presenting a stable kernel ABI is actually pretty unusual.
Did you mean "portable"? Because the ABI to the Linux kernel system calls is very stable. Literally the 32-bit syscall interface has not changed (except by addition) in 30 years.
The darwin kernel is also pretty stable, except Apple moves so fast from one architecture to the next that they drop support for old ISAs pretty fast. I predict they'll drop support for x86 altogether in ~5 years.
I think the GP meant that the Linux syscall ABI is indeed very stable and always has been, as you say, but that the syscall ABI of other OSs is not. So Linux is "unusual" in that sense, not in the sense that its syscall ABI is stable now but wasn't in the past.
AFAICT, SunOS (Solaris), BSD, and Darwin have stable kernel ABIs for the core (UNIX) system calls. I've never used the Mach syscalls on Darwin, but I can imagine those changing more often, because Apple does that. I would imagine that Windows supports 32-bit syscalls still pretty well...but I stopped using Windows in 2002, so tbh, not sure.
Then there’s also the Linux-specific issue of vDSO, which are not kernel code and to which the article applies in full (see: “debugging an evil ho runtime bug”).
> The official API for Illumos and Solaris system calls requires you to use their C library, and OpenBSD wants you to do this as well for security reasons (for OpenBSD system call origin verification). Go has used the C library on Solaris and Illumos for a long time, but through Go 1.15 it made direct system calls on OpenBSD and so current released versions of OpenBSD had a special exemption from their system call origin verification because of it.
On UNIX clones, written in C.
Win32 isn't libc, nor the mainframes ones are (language environments), or Android (Java)/ChromeOS(Brower APIs).
I'm less familiar with the Microsoft ecosystem, but I'm pretty sure it is; Microsoft named their libc MSVCRT.DLL, but it's still the canonical system ABI, and NT, AIUI, actively shuffles system calls between builds to prevent people even trying to use them directly.
The rest, and your general point, are fair, though; there's nothing preventing the creation of a system that doesn't work like this.
It is not an API to the OS.
That isn't just about the lack of guaranteed stability, as far as I understand it is considered a security feature. System calls that are not made through the libc may be actively blocked. So an attacker can't just use an exploit to inject a system call into a process, the call has to pass through the libc wrapper and in combination with ASLR that wont be trivial.
If by "the OS" you mean "Linux". On Windows, calling syscalls is explicitly unsupported and Microsoft will break you mercilessly by changing syscall IDs between versions. On macOS and iOS, libSystem.dylib is the only supported way to call syscalls; syscalls are SPI and Apple will break you if you try to call them directly. BSDs likewise discourage you from calling syscalls.
The "syscalls are stable API" principle that Linux is famous for is a weird Linux-specific thing; other OS's don't do it.
To enable the same kind of container image portability between kernel versions that Linux has, Microsoft has decided to stabilise the kernel syscall ABI.
> Not necessarily. C is how your language interacts with the OS if you use the OS's C library. But not all languages do. Go doesn't.
Go doesn't interact with anything written in a different language? I find this really hard to believe - it would result in Go not having a GUI library, not being able to use PostgreSQL, not being able to start external processes (like shells), etc.
I'm pretty sure that Go can do all those things, hence Go does interact with libraries written in a different language.
That's not what I said. I said Go doesn't interact with the OS using the OS's C library. But, as others have pointed out, that's only true on Linux.
You say that like it's a bad thing. The situation has gotten much worse for third-party languages these days, with frameworks being written Javascript, Swift, etc., which are hard to bind to without fundamental impacts on your language design.
And there are gratuitous differences in C ABIs, for sure. But often there’s good reasons. X86 had no register arguments because few registers; same reason it had no thread pointer. RISC architectures had weird limitations on unaligned access, etc. affecting struct layout.
Maybe I’m being dense. What does a good alternative look like? CORBA?
Specifically, I think it'd be very reasonable & possible to have an ABI that has various floats & integers (specified width of course), and pointers & structures, that has consistent conservative padding on all architectures and precisely one way any given structure is laid out in memory (ok, maybe two, with LSB & MSB integers; but I think that's literally all the variation there is (and MSB is nearly dead too)).
Not the most efficient thing on all architectures, but for language inter-communication it shouldn't be too bad to lose a couple nanoseconds here and there, and I'd guess it'd outweigh the alternative of needing more generic (and thus, less optimized) code handling each individually.
* Use fixed-size types instead of the 5 types for 4 integer sizes (is long 32-bit or 64-bit?)
* Allow multiple return values [so you could use multiple return registers, especially on non-register-starved arches]
* Allow unwind ABI (C has no way to permit unwinding!)
* Dedicated types for pointer + size for arrays
* Dedicated string type (i.e., distinguish between &[u8] and &str in Rust terms)
* Something that allows for vtables would be nice
* Something that encodes ownership (and eventual deallocation) is also potentially valuable
There’s lots of alternatives, MSR wrote an OS in C#
I doubt much non-.NET software runs on that C# OS either.
If you want multiple programming languages to be able to sanely run within a single OS (and do more than just pure computation), though, you have the ABI problem, and this is what the whole discussion is about.
When it was announced it supported about 27 languages.
From Microsoft themselves, J#, C#, VB.NET, and Managed C++ (later replaced by C++/CLI).
It was WebAssembly before its time (among many others), with tons of features that WebAssembly is yet to support.
Also, keep in mind having alternatives isn't necessarily better, as it introduces a problem itself: it complicates things (the article even mentions a problem of this sort). Does having 100 programming languages, with dozens of OS's, each all doing mostly the same sort of things in different ways - simple? Might the programming world be simpler if there was 1 programming language/OS/way of doing something, rather than having 100s of alternatives? (This is just food for thought to demonstrate the point, not something I'm particularly advocating)
Isn't it dependent on what IR do you use?
You know you could make your point without being condescending and trying to insult the author.
They are free to write a new os in rust with superior abis if they want to show us all how it's done.
I'm sure it will be in use by the entire world in 50 years the way c is today, and no one from that time will write a blog post like this one because he of course will get it right.
Do you really think that Unix and C are the absolute pinnacle that our field has to offer, we shouldn't bother trying anything else?
(Not a unix or C hater, just a bit of a tangent about how it's cool that people make new things even if they won't be perfect).
Unix and C are not the pinnacle of ease of use, but rather the survivors of the past 50 years of programming language and operating system evolution. So many of their parents, relatives, competitors, and even children have died and yet they remain.
That means that they are the most adaptive and the most successful in a Darwinian sense, but not necessarily the most ergonomic or user-friendly.
Unix is not technically incompetent (certainly given the needs of the times it was designed in), but its real success now is largely based on it already being an incombent, and being open enough for projects to work from.
Original Unix is dead, it lives on through copy-cats, and they copied it mostly because an experienced user-base makes adoption easier.
In other words, the only time Unix was primarly selected for technical reasons was very short lived. Now its technical benefits are little more relevant than the technical benefits of a qwerty keyboard.
>> Unix is not technically incompetent (certainly given the needs of the times it was designed in), but its real success now is largely based on it already being an incombent, and being open enough for projects to work from.
I disagree.
The technical design of Unix and C as the implementation language were major factors to its success and versatility.
Eric Raymond describes the design choices and culture of Unix that helped make it successful:
* Rule of Modularity: Write simple parts connected by clean interfaces.
* Rule of Clarity: Clarity is better than cleverness.
* Rule of Composition: Design programs to be connected to other programs.
* Rule of Separation: Separate policy from mechanism; separate interfaces from engines.
* Rule of Simplicity: Design for simplicity; add complexity only where you must.
* Rule of Parsimony: Write a big program only when it is clear by demonstration that nothing else will do.
* Rule of Transparency: Design for visibility to make inspection and debugging easier.
* Rule of Robustness: Robustness is the child of transparency and simplicity.
* Rule of Representation: Fold knowledge into data so program logic can be stupid and robust.
* Rule of Least Surprise: In interface design, always do the least surprising thing.
* Rule of Silence: When a program has nothing surprising to say, it should say nothing.
* Rule of Repair: When you must fail, fail noisily and as soon as possible.
* Rule of Economy: Programmer time is expensive; conserve it in preference to machine time.
* Rule of Generation: Avoid hand-hacking; write programs to write programs when you can.
* Rule of Optimization: Prototype before polishing. Get it working before you optimize it.
* Rule of Diversity: Distrust all claims for “one true way”.
* Rule of Extensibility: Design for the future, because it will be here sooner than you think.
Source: http://www.catb.org/~esr/writings/taoup/html/
It was not openness alone that contributed to the success of Unix. There were other open systems, but they did not survive.
Yes, those things did contribute. Originally.
But once Unix won, all it needed to stick around was momentum.
> I always encourage people who are "learning computers"
Buddy, the author is a major contributor to the Rust project
This is probably the most technical post I've seen on HN in a while and accusing someone who goes fairly deep into the implementation details of compilers, and OS libs of 'not understanding computation' is somewhat bizarre.
Damn beginners!
Does the Rust compiler count? The author has been a long time contributor.
> I kind of think of this as the "I like computers but don't really understand computation" fallacy.
You couldn't be more wrong about the author
C is kind of a horrible way of specifying machine interfaces, because it tries to lift things into being portable when the interface is anything but. The ABI is what ties C to the specific machine interface that it’s exposing, and in doing so dooms the ABI from ever changing. The article has many, many examples: a simple one is that it’s stupid to expose a 32-bit value as an “int” because that means “int” needs to be 32-bit forever. In this sense providing a C API is bad because the interpretation of the API into ABI is dependent and on your tooling and sometimes even that doesn’t agree, with fun™ results. This very much isn’t a request for “please give Rust a nice interface to the kernel” but more a “there ought to be something better than having to run libclang just to talk to the kernel”.
This was more an observation that you can specify a system ABI in a way that isn't a fragile as C's. At least it isn't xml plists :D
I suggest looking up the author's CV.
Memory safety etc is a complaint about C but worrying about ABI breaking is literally what you'll always get when you do anything other than assembly[0]. The author laments that Rust and Swift must speak to C but that isn't because of the K&R controlled cabal, albeit it might have been the initial reason. Today the reason everything must talk to C is because operating systems are written in C and expose their API and ABI in C. Write an OS in Rust or Swift (lol) and then get mass adoption and then you won't have to worry about interfacing with C anymore.
They do eventually touch on that, but as long as OS'es are in C then you need C. There unfortunately is no alternative.
[0] I mean technically, you have "ABI breaks" in ASM too, it's just the program goes it's merry way being zombie like until a seg fault happens or worse.
What this article fails to capture is the fundamental question that is faced any time a new architecture is encountered: should 'int' be sized (number of bits) according to its original (or most recent) de-facto definition? Or should it be sized according to the natural register size of the architecture's general-purpose registers?
A lot of us went through this back in the mid-2000's when AMD64 (x86_64) came out. It took both Microsoft and the Linux crowd (just to name two communities) time to come up with their respective (and incompatible) translaitons of types. The top answer to this StackOverflow question summarizes this well:
https://stackoverflow.com/questions/384502/what-is-the-bit-s...
In the gaming industry, we came up with our own typedefs until the standards caught up. Things like int_8, int_64, etc. I say only a fool would ever use somehting as pretentious a concept as 'intmax_t' in normal code (which isn't a tranlation table of typedefs based on platform). "max" according to whom? That is not the way.
Who specifies how you talk to the OS, and how native applications talk to each other? The OS does. And it does, in fact, differ across different OSes (with different calling conventions). The only reason C comes into this picture is because it runs on all the platforms these other languages do (and many more), so you can write an adapter between the language and C, and not have to worry about supporting 10 million different calling conventions, because some C compiler author has done that for you.
So the article is half right; this isn't a programming language. But C sure is.
(And that's not to mention the fact that to some extent the ABI and calling convention is determined more by the CPU architecture than the OS, much less the language!)
[1] Yes it is. It is lacking many important features. And people claiming they rather not have these features are like an author writing a book with notepad.exe because they think modern Word processor are too complicated.
And spell checking is a good thing.
Ok - I get that. But, there is a system call interface. int 0x80, syscall.
Now, these can be wrapped, and the result exposed in a completely different way. But that is the definition. If its "C" on one side and "C" on the other... well, ok then! I though Linux had vDSOs to allow direct "ABI" calls for performance reasons.... utilizing that will force a certain "C-ish" look. In turn, that can be wrapped. None of this changes quickly.
Heck. CP/M-80 had "CALL 5" with registers a certain way. Wasn't "C" by any stretch!
Because the C (POSIX, mostly) "API" is stable and available, we tend to use it. Wasn't always the case -- after all, FORTRAN I/O was all the rage back in the 60s (cf SNOBOL4).
If (whatever) programming system wants to avail itself of the C infrastructure, it is certainly free to do so. Stop the endless whinging about C! Why C? It is the only language in its class that works from Z80 to my Thinkpad.
Go made the mistake of assuming the Darwin syscall interface was stable, and an OS update broke binaries.
On a slightly different topic, few of the problems mentioned in the OP really have much to do with C. I still remember when the interfaces to the original Mac OS (the stuff in Inside Mac, before it got UNIXified) were expressed in Pascal terms. I even vaguely remember MTS interfaces expressed in 360-assembly terms. They were absolutely no better. There is an art to making ABIs and APIs and SPIs and network wire formats and on-disk formats future proof, but it has little or nothing to do with language. The problem is that a bunch of such interfaces exist out there - because they need to exist in concrete form to be useful - that weren't defined with that level of care. It's not much more than coincidence that C happened to be a dominant language in many of those times and domains.
An ABI based on a more "modern" language that involved more detailed types or (even worse) garbage collection would be far far worse for anyone using any other language. C has plenty of problems, but as a notation for expressing an ABI (much like Algol is still sometimes used as a notation for algorithms) it's really not bad.
The inflection point came when "everything became a PDP-11/VAX" which at least made formerly expensive features affordable and set off a wave of innovation that required some kind of standard (especially the Motorola 68000 and the Intel 386).
Now we are so much further along, everything is starting to look like Unix in one form or another. To me, it wouldn't have mattered if the operations systems had converged on NT or VMS, the underlying demand was for Unix like systems and that is where we ended up, to the point where Microsoft have embraced it with WSL and all the big, proprietary OSs of the past are dead or irrelevent.
Sure, the ABI problem is real, but 40 years ago this outcome wasn't guaranteed and it's turned out to be absolutely magic in terms of the proliferation of new languages. From my perspective, everything "having to speak C" at a low level is a feature, not a bug. It might not be a perfect model, but it works and a lot of people are familiar with it.
That de-facto standardization is what makes modern computing possible.
Had UNIX been sold at the same price as the competition instead of free beer tapes, and we would be probably using some BLISS, Mesa or PL/I variant instead.
I am perfectly in scope, given the historical mess that has driven us into this state.
That is the only reason why are even discussing about C to begin with.
[1] https://en.wikipedia.org/wiki/Interface_description_language
Regarding features, it’s clear that you’d need to find some sort of common ground, though not necessarily just the lowest common denominator.
It's easy to complain about C or C++, but I don't see that many languages that compiles to machine code, and offer compilers for many platforms.
I agree that C is somehow the new "assembly".
C is the glue used for building operating system, so it's not surprising you need to use it in many places.
I'm sorry but I don't see any better language that is as simple to learn for students as C, and fixes some problems of C. Most new languages are often difficult to read, introduce a lot of un-needed sophisticated features, and are not that much used because they're too niche.
A good example is Rust. Sure, the safety is an awesome feature, but ADA already did it somehow, but it's not what developers need.
There is a reason the world talk english and not german or japanese. It's because english is just much, much easier to learn. Nobody cares if the language makes sense.
It's odd because HTML and javascript are so much more ambiguous and cause a lot of pain, yet I hear more complaints about C or C++.
HTML implementations (and to extent JS) are very forgiving - you could have thousands of warnings and errors raised by linter, but page will still render mostly fine. In this regard C would be closer to XHTML which would fail to render unless it's "well formed" and, well, nobody uses it nowadays.
The C wrappers generated by MIDL are also fairly similar to raw C++, compare (where pUnk is an IUnk *):
pUnk->Something(&bA);
with... IUnk_Something(pUnk, &bA);
which expands to something like this: pUnk->lpVtbl->Something(pUnk, &bA);How does a language "try to be a good ABI"? An example of such a language would help.
An ABI doesn't need to be a language. In fact, I'd say that makes it worse, as now you'd have a potentially conflicting goal, wanting to make it nicer for the language itself, possibly at the cost of making for a worse ABI.
But one could do well with not having varying width integers (looking especially at you, "long"), having a defined structure layout, and one (primary) calling convention per architecture (it not being the absolute best if goals change would be fine, as it'd only be used to talk between different languages, and that usually doesn't happen with such a frequency that a couple nanoseconds hurt). Sure, you'd still end up with some variance, but it'd be a lot easier to work with, and it'd be pretty hard to reach the count of 176 ABIs of C.
edit: ..and just not defining things that aren't actually related to ABI (or may need to change), i.e. intmax_t
I don't think that that is correct. Sure, your C implementation defines which params go into which register in a certain way, but other C implementations define the same thing in a different way.
A * B; And (A) - B Can only mean one thing. Sure this wouldn’t do anything for the old code, but at least where new C code was required we wouldn’t continue dealing with all of its complexity.
I feel like this is the same problem with CSVs. It is trivial to parse if people follow the standard but most people don’t. But if you create a parser that only accepts the standard and refuses to parse non standard CSVs then you never have to deal with any of the BS edge cases that come up. And if you show where in the code/CSV the ambiguity is, people might actually fix their code to get it to parse correctly instead of leaving it ambiguous
C is a high-level programming language not unlike, say, Fortran, and its power and flexibility is in expressing calculations and concepts rather than in representing the architecture of a particular processor or in how well-optimized the generated machine code is; compilers are very good at both, and, frankly, they should be expected to do a better job than a human could in a reasonable time - especially when code is still in flux or needs to be refactored.
But the image above that is calling conventions for functions on a particular processor. What does that have to do with C?
> So actually semantically parsing a C header is a horrible nightmare
I'm confused. It seems to me the right way to go would be to improve C such that it's more precisely specified, easier to parse, etc. Genuine question: why is that so hard? (I suspect the reasons are more institutional than technical.)
Variable declarations are syntactically weird in C, for example. And newer languages seem to have better ways (that are actually context-free). So why couldn't C be evolved toward that?
> int foo(int x, int y)
becomes
> foo(x: i32, y: i32) -> i32
and so on.
> You don’t see England trying to improve itself, do you?
I guess that's a saying I'm not familiar with. Seems to me England has improved a lot over the years.
Go easy on me, geniuses.
It's not hard to specify a language that is all those things compared to C, but then it is, ipso facto, not C.
And we’re talking about something from the premodern time of C. Changing how declarations are written now is a complete non-starter; there would be absolutely zero appetite for that, and even if the standards committee did it, everyone would just continue using compiler flags for the last version of the standard before the change happened, so compilers and parsers would have to continue supporting it anyway.
> Surely C has deprecated something
Again, deprecated yes, but the only thing I can actually think of them removing outright is the gets function, which had way more potential to cause harm than these parsing issues.
I think you are just massively overestimating the extent to which the C committee is willing to break things. The language is managed and standardized in a totally different way from things like Python.
But C syntax really isn't the biggest problem in this. Even if you decide to write a whole new C parser for your new language, you'd still need to support the 176 ABI triples (or not work on many systems) and detect & choose which one the user's system uses. It wouldn't be hard to reduce that number by a lot if anyone actually would've tried to.
Interesting! I stand corrected, then.
Also, it seems to me that anyone who wants the best for C would want to get rid of the context-sensitive parsing. So that would include those on the standards committee. Here's hoping they can continue improving the language over time, albeit slowly.
This is why pure C projects can turn into an unmanageable behemoths. This is also why the best way to use C is to use it to implement core algorithms and data structures which can then be called by higher-level languages. Numpy/Scipy did this perfectly and now their use is now ubiquitous within the Python community.
Most software engineers I know who have a background in EE love C, simply because it maps very well to what a processor actually does during execution.
I agree with this. One thing to note though: C maps onto small CPUs pretty well. It doesn't map directly onto the x86_64 execution model at all.
Today's fast CPUs are very different than the abstract machine that C represents. They run all kinds of things out-of-order, do speculative execution, run instructions in parallel, engage in branch prediction, etc. If anything the C execution model is almost a like a little VM that gets mapped onto the core that's running it.
[citation needed]
> the best way to use C is to use it to implement core algorithms and data structures
is this a joke. you literally cannot write containers in C unless you commit to heap-allocating everything and storing it as void*
> Most software engineers I know who have a background in EE love C, simply because it maps very well to what a processor actually does during execution.
lol. no it absolutely does not. i have a B.S. CpE and have actually built simple processors. the C execution model has nothing to do with how silicon operates, and modern silicon in particular goes to absurd lengths to put up a façade that c programs can use to pretend they're still on a pdp-11 while the processor goes and does other things.
easy example: here's a memory address. what happens when you try to read from it
>lol. no it absolutely does not. i have a B.S. CpE and have actually built simple processors. the C execution model has nothing to do with how silicon operates, and modern silicon in particular goes to absurd lengths to put up a façade that c programs can use to pretend they're still on a pdp-11 while the processor goes and does other things.
I'm an EE who's been writing bare-metal firmware for over ten years, and who's helped develop memory subsystems for microcontrollers. What you're saying is certainly true for PC CPUs, but the C execution model works just fine for a Cortex-M or other low-end CPU. No "absurd lengths" are needed; there's a clear relationship between:
MOV R2, #0x400
LDR R1, [R2, #5]
and: volatile uint32_t *b = (volatile uint32_t *)0x400;
uint32_t a = b[5];
>easy example: here's a memory address. what happens when you try to read from itThe CPU puts the address on the address lines and sets some control signals appropriately, and the SRAM returns the value at that address on the data lines. (Simplifying, obviously; I'm not going to go dig up an AHB spec.)
Cache? What cache? SRAM reads are single-cycle. The flash memory probably has some caching, but that's in the flash subsystem, not the CPU. And a lot of your most important reads and writes will be to memory-mapped registers, which had better not be cached!
No, C does not express the details of the instruction pipeline or complex memory subsystems directly in the language. Neither does assembly. C also does not cover every CPU instruction -- that wouldn't be portable at all. That's what inline assembly and compiler intrinsics are for.
C strikes a balance between portability and closeness to the hardware while remaining a small language. It does this very well, which is why it has historically been so popular, and still is for some purposes. Not all software is CPU-limited data processing on a 64-bit server.
Runs just about every computer from the tiniest microcontroller to the largest supercomputer. Has been doing so for 50 years, despite a constant parade of miracle languages that were going to replace it Real Soon Now.
When Miracle Language of the Day actually does what C does, across the same variety of hardware, I will be the first to congratulate it and its designers.
But I don't think that's going to happen any time soon.
It can also be seen on another axis, that of organic growth versus what each new miracle language tries to be, which is a centrally planned and all-encompassing solution, which isn’t possible without the entire rest of the industry just stopping and waiting for it all to be made to work.
C already works. So people work with it.
You shouldn't need a "miracle language" or even an "all-encompassing solution" to have a chance to break free of this.
That is nonsense.
As I noted below, low-level OS internals have been reimplemented numerous times, by small teams of volunteers at that.
If you want to "break free", buckle down and do the work.
But if you actually want to program something usable in conjunction with existing software, such as Linux, you need to use the C ABI. There is no alternative.
Who cares long as the interface is type-safe?
Good joke, yeah. The whole "can you give me a source for that?" thing is getting quite old.
Oh, what? You were serious?!
These things come up in C first because C is the lingua franca.
In C++, the next mistake is going to be the hardware_{constructive,destructive}_interference_size constants that have serious ABI implications. I think that current GCC position is that they are not part of the ABI and they are subject to change (but also controllable from the command line).
I submit that this Tower of Babel truth is older than C and a reflection of the humanity that created the tool, not the tool itself.
Minimize the entropy and be at peace with existential imperfection, say I.
Who knows, in the future maybe wasm will be the common ABI between languages.
Sure they can, you just need to build the mechanism that allows them to.
That man page is for glibc. You instead want to look at man syscalls and man syscall to look up how to interface with he kernel. Linux does not require programs to use any amount of C.
The second sentence is:
> System calls are generally not invoked directly, but rather via wrapper functions in glibc (or perhaps some other library).
Okay, so we can't avoid C that way, but what's the "invoked directly" way? Well, it's syscall(2): https://man7.org/linux/man-pages/man2/syscall.2.html
This man page actually reiterates what the other one, that you shouldn't be making syscalls directly! But in any case, what is `syscall()`?
...it's a C macro.
So yeah, Linux does require programs to use C. You've just been isolated from it.
Zig solves interoperability by incorporating an entire copy of LLVM. Go does do bare metal syscalls, but as mentioned elsewhere in these comments, this has caused breakage on Mac when the kernel was updated, because this interface isn't stable.
Programming language authors, writing their own stdglibbabbyscript, are exempt from that warning? That’s of course a big task nobody wants to do so many of them just piggyback on glibc with thin wrappers.
No, in the syscall man page it tells you how to call a syscall. For x86_64 you put the syscall into rax and then use the syscall instruction (syscall is the name of an actual instruction). That man page includes more information like what registers arguments should be in or where to find the return value.
If you've ever designed hardware and had to bootstrap it up to a "high level" situation - then these are nonsense concerned when you are merely trying to get a new hardware design up and running. You can always bootstrap to a language that already has all these things. The author obviously has never done anything like that. Not based in any reality where C matters.
Hardly an argument, in my opinion, and it's annoying to see parroted on every thread about C's syntax.
Henry Ford
The compiler also does the ABIs for several targets, even more if you include the LLVM and GCC backends.
C is a shit old language but we have basically tamed it.
And open was just an (easy) example. Now do that for xcb_create_window and all the other functions of all the system library you want to interact with.
.. but what does open() call? Do you think that's air you're breathing?
(The GP is implying you can just call directly into the kernel via a syscall,.. and all you need to worry about is your OS's syscall interface. You don't necessarily need the C ABI; unless of course your OS's kernel uses it for syscalls, at which point: sorry-not-sorry.)
What’s the problem with the ABI?
Universities are too academic and companies are too selfish or busy.
Writing native modules for Java, Node or anything else you run into these shenanigans which are very costly to the world.
I really do think in 2021 that G, MS, Nnvidia, Intel etc. should get together and create something simple and clear for ABIs and more importantly get everyone on the program.
That, and Javascript 2, which dumps a lot of its weirdness and comes with a comprehensive standard lib.
First. An ABI had nothing to do with C, it is an OS/hardware calling conventions - how the stack is used and procedure's parameters being passed. That is a machine code level.
Second. Redefinition of hardware types is beyond bullshit. I can't even comment on this.
Third. Yes, Every. Single. Architecture will have its own ABI. This is how linux kernel is organised (as a direct consequence of the above) and it has nothing to do with C. Again, it is a machine code level.
The rest is not worth commenting.
I missed the fact who wrote the article, so I am ok with being flagged in this discussion. This is the new normal.
But to go on and assert that it "has" to be this way, or that its this way strictly out of necessity... I don't know how to describe it politely without saying "stockholm syndrome". What is it about C, or about a certain subset of C-fans that results in them asserting that 'it has to be this way' or tossing derision at those who strive for better? Do some of you realize that is born out of living in an ecosystem where we don't have to deal with this bullshit and thus don't see it as "normal" the same way you do?
It's okay. I've watched the exact same type of excitement faced with derision on countless technologies in the past. Oh how wrong I've been to bet on [insert cluster technology here] or [insert linux service manager here], surely those would never take off because hand-maintained bash scripts are fine right? Just like an undefined fragile ABI is fine, right?
It kind of makes me think of some long screed I read last night from someone making alls sorts of claims about Linux ond the Desktop while insisting that they use Xorg. Meanwhile, under Sway/wayland, I am driving 3 monitors at different resolutions and refresh rates and they all perform perfectly on testufo.com).
Your trauma doesn't make your choices better. Your supposed growth in the face of now-unnecessary pain doesn't make you smarter. I have contributed code that runs on thousands of peoples desktops every day, in both C and [insert language here] and I cannot believe this is still a fucking conversation. I mean, thankful for one side of it, but sad that in yet another facet of life there are folks insisting that the shit-we-have is the best possible. Sad.
You don’t have to write FFI for Linux. Use your great new language to write an OS, or one of the many OSes not written in C.
Let’s face the facts, your new language probably solves some pretty theoretical problems and not problems people actually have like, hey I need to layout these structs exactly so the hardware will work. Or I can parse a packet off the wire in a reasonable amount of time.
And sure, because of the way things shook out, programming languages have to speak C in order to do something useful as fast as possible, but if you carry this sort of argument to its logical ends, it amounts to the same thing as saying “I would love programming if only I didn’t have to compile to machine code” ASM has the same problems—architectures have different instruction sets. Or further, this is like complaining about the fact that you need to code against a particular fundamental design scheme like von neumann. This is just the nature of computing and different ideas and markets yielding different products.
Also this article makes a mistake of criticizing C on the grounds that it’s now a protocol and poorly designed as a protocol while simultaneously admitting that it wasn’t designed to be a protocol—so it seems a bit unfair to call these “bad design decisions” on those grounds. Furthermore, it’s a lot easier to make these sweeping criticisms than to effectively solve what is not only a massive technical but also social problem.
This is a really naive take.
In aerospace design we have the concept of "Design for Human Factors" or "Human Factors Engineering" which is basically an acceptance that the pilot or crewmember is a fallible, flawed "system".
That is to say in aerospace we recognize that human beings have limits. They have finite breadths of attention, and they panic in a crisis, and they make mistakes when fatigued, and they're not capable of performing repeated tasks 100% consistently.
That's why for example we make the "fuel cut off" switch a different size and color than the intercom switch. That's why we make the engine fire extinguisher handle very prominent and easy to grab, etc.
For how many decades have software folks been writing use-after-frees and off-by-ones?
When is Software as a field going to get over its collective hubris and admit that C is poorly designed from a Human Factors standpoint? A tool that requires unattainably- or unsustainably-high human performance is a shitty tool; get a better one.
I also would not recommend writing any large-scale modern software in C unless absolutely every piece of the codebase is performance critical, which is highly unlikely, and in which case you can probably use c++ for at least a few more safety features. Is C still a good choice for simple CLIs and small programs that don’t do anything drastic? I’d argue it is.
I was moreso using hyperbole to try and point out that it’s easy to judge our forebears for problems that we didn’t even know were problems yet. Saying “C is a poorly designed language” usually amounts to saying “C is a poorly designed language from today’s perspective” which is a bit disingenuous considering we wouldn’t even know about these problems without C. It’s kind of hard to be omniscient and design against things you didn’t even know are problems yet. Furthermore, for all the flak it gets, I think C got a lot of things right—it’s easy to forget that programming languages copy more from C than they distance themselves from it—really the only major wins are giving programmers less control over memory, easier numeric types, easier string manipulation, and generics, and go proved that even the last enhancement isn’t really necessary.
Sorry for playing the role of C apologist in this thread, but I feel a lot of these broad complaints about C are sort of immature insofar as they seem to eschew all historical context. I guess I’m just trying to point out that I see a lot more genius in C’s design than I see foolishness, but some people would have you think the C designers were complete idiots with the way they disparage the language. There’s a palpable lack of humility in such complaints.
I mean okay, but whether or not we have historical perspective doesn't change the fact that as we now understand it, C is poorly designed. If it's poorly designed, then it's poorly designed and we should stop using it.
There was a time when we "lacked historical perspective" and we built the De Havilland Comet, too. People died. Turns out that square windows cause stress concentrations so we started using better techniques. Nobody was arguing "yeah but for simple CLIs and small programs that don't do anything drastic, square windows are fine". Square windows suck so we make rounded corners now.
C sucks, we should use something that sucks less.
How?
Take Linux. Currently written in C. Yay! Write a module for it in something else. Good. Perhaps you've heard of Rust? Ok, great. Write some more modules in that. Write even more. Even more. Now there's just a lot of Rust talking C ABI. Hmmm. If only... oh wait. Let's change that. Time passes. Boom! The whole thing is Rust. No C ABI target at all other than for C applications, which is how things should be.
This is one way to proceed and it may have already begun.
https://www.zdnet.com/article/rust-in-the-linux-kernel-why-i...
In reality C is a classic and has weathered many storms, fads and changes in fashion. Even with other languages in the linux kernel it will probably stick around for a lot longer.
And I chose rust at random because its an available systems language. Why not something else? You don't actually need C at the bottom layer. It won because the OS are most commonly written in C, it's encountered elsewhere and therefore popular. Usually because it helps get the desired result. That isn’t nothing either.
In days of olde there was pascal calling convention. Why? The os was written to that, because it was written in that. Surprise! I'm not even suggesting these are bad things.
Linux for me is a kernel. The choice of C is purely an implementation detail based on pragmatic decisions to suit its purpose. I am merely suggesting that the calling convention isn’t locked at all layers for all time. Yes you need a common convention. But it needn't be C and it doesn't need to be C all the way up or down either.
I'll leave it there. Seems that discussing this kind of thing isn't wanted.
Side note: I wonder what would have happened if I'd chosen Lua for my example. Would people have gotten hung up on the fact that its commonly implemented in C? Would they have locked onto that yet not realised that it needn't actually be written in C at all? If you wish to explore that, swap rust for lua in my previous comment. There are ways of using lua as a calling convention without even referencing C. If you want a more realistic example, swap in dart or more likely D. Everything I've said still applies. And yes you can have mutliple calling conventions or even a simpler one. Its just a convention of how to pass parameters, amongst other things. Thats basically it.