Nah it’s a programming language author just don’t like it.
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.
My Google SoC project was writing a c++ bindings generator for Common Lisp: https://lwn.net/Articles/147676/ (In my head this was ten years ago, but apparently it‘s nearing 17. Shit I’m getting old.)
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.
> Yes parsing C is non-trivial, but that’s true of every language.
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.
This is a strange complaint that seems to reduce to C not having a well defined ABI.
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.
Esoteric microcontrollers don't need an ABI. ABIs are for static and dynamic linking, and in the microcontroller world, there isn't a whole lot of linking going on. It's very common to statically compile everything into one single flash image, with libraries being in source form. E.g. this is the case in the Arduino ecosystem.
They still have an internal ABI used when static linking; aligning structures, laying out data sections, function call behavior, and so forth. C's flexibility/ambivalence in this regard is why it's preferred on those architectures.
One could consider static linking in this scenario just an optimization for incremental builds with separate compilation of source files; it's not really used to, e.g. link a library that has no source. In that sense, it's just an implementation detail that's leaked from a build system that notionally recompiles from source but has unwisely exposed the units of its caching system.
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.
I don’t think there is anything wrong with the single compilation unit approach, but every embedded project I’ve been on on the last 10 years has used static linking, which doesn’t preclude ending up with a single flash image. 10 years is about the last time I worked on a non-arm based project, so that could be a factor (the pic compiler I was familiar with at that time did not have a linker, and I’m not sure if the avr compiler I used did)
This. The intention for C was never to have an immutable, well defined super-portable ABI so that all the people who like to feel superior for not using C can paper over their C use in comfort. It just so happened that C is so simple that it is some 80% along the way to an ABI between systems.
I'd argue it's more like only 40% of the way to a proper ABI; sizeof(long) varying on the same CPU architecture is just horrible. Sure, it wasn't meant to be an ABI, but, well, it is. And we're stuck with it. And oh how nice would it be if we weren't.
i mean, if sizeof(int8_t) varied, noone would even try to use C as a lingua franca. I, as the programmer, would never expose unknown width integer types, but other things still will, and I'll be forced to interact with them. Allowing such things to even be present in an ABI is just horrible.
> On the other hand, `sizeof (int8_t)` is the same size no matter which architecture you are running on.
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.
huh, I guess that's still not hard-coded. But a non-8-bit-byte system probably runs near nothing to interface with anyway, and would definitely need a completely separate ABI in any scenario.
CHAR_BIT is required to be at least 8, int8_t is required to be exactly 8 bits if present, sizeof(int8_t) is required to return an integer size where sizeof(char) is 1. Hence, if int8_t is present then CHAR_BIT must be 8 and sizeof(int8_t) == 1.
This is less about stability and more about clarity. The most unchanging and unbroken ABI in the world can still be a huge mess of undocumented special cases only really accessible via the combination of the platform's C compiler and its headers.
The complaint seems to be less that C as a whole has a lot of ABI implementations, and more that those individual implementations themselves are also not very well-defined, even on the biggest platforms.
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.
Linux and Windows have had rock solid ABIs for decades now. Just about any system-appropriate 32bit binary will run on either, if the necessary system libraries are present.
Again, this really doesn't seem related to the article. Nobody is saying those ABIs are making breaking changes, but that they are poorly documented and full of edge cases that make it difficult to reliably produce one of those system-appropriate binaries, short of just giving up and pulling in the platform's entire C toolchain.
Outside of only the most trivial microcontrollers you're going to find edge cases numbering proportional to the complexity of the system. Software complexity is hard.
I'm not sure what's the goal in complaining about that. Just venting?
Just because it's hard doesn't mean it's already been done in the best possible way. Finding and documenting flaws (or "venting") is a great step towards improving.
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?
The author failed to prove that the current way isn't sufficiently good, or even that C is particularly bad in its role. They've only maligned its use and failed to appropriately consider why it's used.
> Linux and Windows have had rock solid ABIs for decades now
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!
Many of these implementations are needlessly weird and underspecified. C happens to work despite these quirks due to sheer amount of hours everyone puts to work around C's critical but vague notion of an 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.
It's not a quirk of C; it's a failure of the vendors and their partners to appropriately coordinate. This outcome happened _despite_ the best efforts of toolchain developers to provide harmony.
The C standard working group is the avenue for vendors/partners to coordinate. Whenever the standard settles on "implementation defined" that's a confirmation that the vendors failed to reach an agreement (often because the standardization effort happened too late, and they've already implemented diverging solutions, and nobody wants to break their userbase).
I think your argument kinda agrees with TFA, as it's stating that C's nature as the defacto ABI for all interoperability on all operating systems is in conflict with it trying to, at the same time, still be a programming language.
It would be very nice to have a non-C ABI.
On many platforms, such as FreeBSD and macOS, the application-exposed ABI for tasks such as "open a file" or "validate this x.509 TLS certificate" mandates that you link to a shared library and call into it using the C calling convention. (libc and Security.framework, respectively.)
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 application-exposed ABI for those tasks mandates that you execute a few assembly instructions to make a syscall (and perhaps also do some work that the libc normally wraps around the actual syscall for you). ABIs deal with machine instructions, that's why the "binary" is in there.
The OS-provided API for the tasks are libc and Security.framework. Do you have any suggestions for a better API?
Interesting rant. I found it fairly humorous but I am not sure if the author meant it that way.
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)
It's less that the OS is written in C, but more that literally every single interface to literally anything is either written in C, or isn't meant to be used by multiple languages. C is the de facto standard for interacting with anything not written in your language. Window management, I/O, graphics, everything has a C interface. There aren't even any alternatives (besides just sending a list of bytes through a pipe or something, which noone wants to do for obvious reasons).
I don't disagree, I see an expectation on the part of driver/API providers that you can write (probably in C) and adapter layer that can talk "glorious language" on the input, and then do the naughty bits and talk to the C API on the output.
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".
I mean, a C interface is better than a machine code interface. The problem with that is that, unless you include a C compiler (or at least a thing that understands the 176 C ABIs) with your language, you can't use it. (not that machine code would be better at that, but I'm sure that if people actually intentionally attempted to make a global cross-platform ABI, they'd easily make something much better than C)
You'd obviously prefer calling into things made in your own language than some other. But, from any other language you'd much prefer a C interface over having to explicitly describe which registers & stack slots to put things in and read from.
I did this once trying to mitigate Go's FFI performance cost. It wasn't great but it wasn't awful either. If I had to do it again I'd generate the assembly trampolines.
There are a couple solid summaries in that thread based on my experience going through that exercise - thanks for the link
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.
> My position is that this is less of a burden than writing something that goes from the "glorious language" into machine code.
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.
> My position is that this is less of a burden than writing something that goes from the "glorious language" into machine code.
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.
> but more that literally every single interface to literally anything is either written in C, or isn't meant to be used by multiple languages. C is the de facto standard for interacting with anything not written in your language.
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".
> C is the de facto standard for interacting with anything not written in your language.
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.)
Again, there's more than the OS kernel that a programming language will need to interact with. You don't go through syscalls to make an X11 window or read keyboard/mouse inputs or draw graphics. There are thousands of utilities that are meant to be usable cross-language, but in actuality use the C ABI (well, at least one of the 176 C ABIs there are).
> 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.
...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.
> 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.
> Did you mean "portable"? Because the ABI to the Linux kernel system calls is very stable.
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.
read is unlikely to change anytime soon on macOS but Apple can and does change its numbering for lesser-used but still very important system calls all the time.
They also change the syscall abi without touching the numbering. gettimeofday(2) is a pretty famous one there.
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.
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.
No, msvcrt.dll isn't the system ABI. It's officially not even part of the OS and you aren't supposed to rely on it existing. Kernel32.dll is the primary entry point for OS things, and it's distinctly not a libc.
Even Go gave up and switched to using libc for OpenBSD[1]. You can use syscalls on Linux because Linux, lacking a blessed libc implementation, guarantees stability of syscalls as an interface. The BSDs, afaik, do not, so circumventing libc is liable to render your binary forwards-incompatible with future kernel versions.
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.
A better way of accomplishing this would have been to randomize the syscall numbers on a per-process basis, and map a "syscall number translation table" into the process when loading the executable.
> 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.
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.
>> C is the de facto standard for interacting with anything not written in your language.
> 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.
You should think of C as a front-end for the computing platform you are targeting. The platform has an ABI, which you need to use to interact with the system components. So because every third-party has to speak that ABI to interface with the platform, they might as well use that ABI to interface with each other too. You can of course build your own little world within a platform, with all your binaries speaking a different ABI to each other. And that might have some advantages, but most people find those advantages outweighed by just using the same ABI as the platform.
> Window management, I/O, graphics, everything has a C interface.
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.
Yes, it is possible to make worse ABIs than C's. That's not a surprise. Doesn't mean C's is automagically the best possible. Something that, at the very least, doesn't have varying sizes for the same type between architectures/OSes, and precisely defines structure layout/padding, would be vastly better. (not saying that's a realistic thing to get at this point, but doesn't change the fact that it'd be much nicer.)
I’m not saying it’s the best thing, but I’m not sure you can get much better. A lot of things defined by the C ABI are things you’d have to do anyway if you’re compiling to native code (structure alignment, where’s the stack, what’s called-save, callee-save, where do you find the return value, etc.) Targeting the C ABI’s set of choices isn’t that different from targeting a different set of choices.
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?
Between architectures you'd indeed need quite a few differences, but for the same architecture it'd be nice if things more or less agreed on things.
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.
The various C ABIs definitely do, though. And you generally know which C ABI you need to interact with (the __int128_t example from the article just seems like a bug -- luckily use of that type is rare).
A lisp machine, by definition, runs lisp. The ABI problem only exists in environments where multiple programming languages exist.
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.
I think that's besides his point, which I take is: Replace C with an alternative and people will complain of similar problems, since all abstractions are leaky abstractions to some extent. Or in other words, since you can never be immune to this "leaky" problem, you will inevitably have similar kinds of problems despite using an alternative. Which is why you'll find every programming language has critics.
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)
>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)
The author invites insult by being insulting themselsves, not to mention comically overweening.
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?
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.
That would be more compelling if their success was more directly coupled with their technical design.
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.
>> That would be more compelling if their success was more directly coupled with their technical design.
>> 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.
It not being possible to improve the situation doesn't mean that the current situation is good. Is the article's rant gonna achieve much? Most likely, nope. I'm fairly sure the author recognizes that.
no but even though this is written in a rant-ish style it isn't isn't really one given that the post is full of specific examples discussing all the things that are wrong with C as an interface.
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.
I think it's been a few years since Gankra has been active, but she was the original mastermind of std::collections as well as the original author of the Rustonomicon, if anyone wants her credentials.
I only made it about 80% through because, yes it was a rant. I'm pretty sure the author knew what they were talking about. The issues discussed were those of someone who knows how computers work with a great deal more depth than many (if not the vast majority) of developers. The point that programming languages are just syntactic sugar around machine code isn't really relevant here since, for better or for worse, C headers are the standard way to codify access to ABI's. A deep understanding may be fine if you aren't accessing many ABI's, but it doesn't scale well. Using C headers does scale well, but the author is pointing out there are many things about C that makes this difficult.
Agreed. That part is just mind-bogglingly awful, and fairly unique to C/C++. AFAICT all the other problems are common across most alternatives, many of which are actually worse.
Putting aside the personal jab, which the other comments have rightfully called you out for (really, this is why people detest “the orange site”) you’ve just fundamentally misunderstood the point of the blog post.
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”.
So what do you suggest instead? Express APIs in yet another language (perhaps one with defined-length numeric types) other than the ones people actually use? An IDL? Who's going to do that work, from the definitions themselves to the bazillion language-specific bindings? I'm not saying that you should have to have an answer before you can criticize, but every solution to this particular problem sucks in some way. Some things are just hard. Jumping on a bandwagon to tear down something that already exists, ignoring or trivializing any problems it does solve or that alternatives would need to, is another much-despised Orange Site behavior. Nobody ever turns out to have a real silver bullet in their back pocket.
Well, you can tell why the orange site put it on the homepage, right ;) But to respond in seriousness: yes, the blog post exists to mostly highlight a problem, and it doesn’t have an easy solution. But there’s a couple things you can do to help: one is to define a sort of “restricted C” that is easy to parse and designed to be ABI stable, which I think Microsoft actually tries to do. You could use some sort of IDL, yes: web browsers do this for their “system interface”, and as ‘olliej mentions so does Fuchia. But the main point is to shed some light on the issue to start a discussion, because until now people have mostly been OK with defining everything using C and it’s been causing problems that nobody’s really thought about trying to solve, and I think the post does a pretty decent job of that.
> 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)
I have an old lapel button: C combines the power of assembly language with the flexibility of assembly language. Humorous, yes, but still true - it's a high-level, platform-independent assembly language that (used well) allows for creating all sorts of things great and small. I found it the perfect back-end to a Python front end, where the known problems of C could be minimized via control being done at the interpreter level and the compute-heavy work being done in C.
Indeed! I recently hacked up a considerable amount of assembly and used a lot of tricks that would not be possible unless I used copious gotos in C and also non-standard tail-call optimization. Some tricks (like writing inline/out-of-line codepaths) quite literally are not possible in C.
I would also add then that assembly language is not as powerful or flexible as VHDL.
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.
Vectorized numeric kernels still often need to be written by hand, for one example of where modern compilers just aren’t good enough to make writing assembly obsolete.
I think this falls into the category of articles where the author makes an eye-catching assertion in the title to pull you in and then totally fails to justify it other than by ranting about some personal gripe he has.
I'm not clear on why this is a C problem specifically, when it sounds like the author is really bothered by the lack of standardization in ABIs. Why would other languages not have this problem? At the assembly level, you still need to know which parameters go in which register and how the stack is laid out in memory. Rewriting the Linux kernel in Rust wouldn't change the way that software interrupts work, so how would it give you a Rust-native system call ABI? Am I missing something?
It's not a problem with C itself. But it's a problem that C, which has loosely defined ABI, is actually used as the standard ABI of the OS.
And the OS is not only about system call, but about system libraries (window manager, ...), and interoperabiloty between languages
As noted very clearly in the article, the problem with using underspecified versions of C as the ABI is _also_ a problem for C the language, as it limits the possible evolution of C.
ABIs are only a problem in environments that use dynamic shared objects. But the reason why that's the case isn't obvious. So when in doubt everyone just scapegoats C. I suppose the strongest language can take the punches.
I literally made it to the third sentence. How is using something other than C going to fix ABI? Isn't it literally the same problem? Do other languages super mangle their symbols so there's no breakages anymore?
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.
I disagree with the author. C is an amazing language but its "lack" of an ABI is, in some ways, a byproduct of the language itself. C essentially sits half a layer above assembly and is meant to give the programmer fine-grained control over the processor and memory without requiring intimate knowledge of x86/ARM.
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.
> 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.
> 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
>> 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.
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:
>easy example: here's a memory address. what happens when you try to read from it
The 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.
Also the "PDP-11 facade" is needed so that the thing can be programmed in assembly language without the board support people doing bring-up tearing their hair out. A sane assembly language that you can read instruction by instruction to understand the abstract effect is necessary for more than serving as a C compiler target.
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 runs on all hardware because hardware manufacturers support it, which they essentially must do because that's what's expected. It's a self-fulfilling prophecy (and arguably a vicious cycle).
Or, it’s a virtuous cycle of people recognizing a valuable tool and giving back to its ecosystem by developing for it (even if that’s not intended–it’s a second-order effect).
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.
It's "organic" because, as this article is pointing out, creating alternatives is really difficult due to this exact lock-in, both at the OS level and at the hardware-vendor level.
You shouldn't need a "miracle language" or even an "all-encompassing solution" to have a chance to break free of this.
Sure. You can re-implement everything starting with the Kernel, as long as you don't have to interface with any of the C microcode on the hardware itself. And, yeah, people are doing this, for instance with Redox OS.
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.
> My problem is that C was elevated to a role of prestige and power, its reign so absolute and eternal that it has completely distorted the way we speak to each other. Rust and Swift cannot simply speak their native and comfortable tongues – they must instead wrap themselves in a grotesque simulacra of C’s skin and make their flesh undulate in the same ways it does.
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.
Hmm, very interesting question. I think you could interface with X windows via IPC, but you'd have to build that IPC layer, which ultimately boils down to pipes/sockets. So that route would be doable, but probably a steep climb.
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.
But how would you link a library written in Rust to a Virgil program? Or vice versa?
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.
> But how would you link a library written in Rust to a Virgil program? Or vice versa?
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".
Your argument reads like: you shouldn't need to run Rust programs unless _the entire operating system_ is first rewritten in Rust. I doubt you really mean that, though...
C definitely doesn't provide a well-defined stable interface. Specific ABI definitions might, but there are 176 of those, and implementing all of those in another language is decidedly not what I'd call "easy".
In most cases, I'd think those small teams support like a max of 5 ABIs, out of the 176.
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.
Not sure what the point of this rant is. C is an old language. I remember switching from Motorola assembler on my Amiga to ANSI C on my new PC and complaining how high-level it was. But it was so much easier and faster to write more complicated software with it. So it became popular and operating systems were written in it.
Now we have many even higher level languages that abstract a lot of the real computing that is required for CPUs to work, and people complain every time they get exposed to the low-level stuff. Well, sorry, but on the very low level computers are actually quite complicated. Not everything can be abstracted and hidden from your eyes. As CPUs become more powerful and our systems have more RAM, we can be more wasteful with how we use these resources. So more and more of the low-level stuff can be hidden. But there will always be value in programming in C in ways that take full advantage of the hardware (especially for simple, battery powered devices).
A lot of the comments here are saying that C not having a well-defined ABI is acceptable/good. For people writing C, yes, that may very well be a good thing. But as the article states, pretty much every single language must interface with C to be usable, and, for them, it means either calling out to clang/gcc/tcc, or going through the mess of transliterating the C ABI of each target to your own language. not fun.
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.
What you're complaining about, as has been pointed out by several people elsewhere, is that actual real world platforms have different C ABIs. This isn't C's problem, it is a problem that C exposes.
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.
A lot of the outrage against C seems to be based on the assumption that it gained its prominence either in a vacuum or in a modern context. Oh, young ones, such is not the case. C is a creation of its time as is everything else like Linux and TCP/IP and SCSI. All of their warts seem obvious in 20/22 hindsight, but they were either not obvious at the time or were outweighed by other contemporaneous needs/constraints (usually lack of compute or other power). Bashing C without acknowledging its context and place in computing history only shows an author's shallow ignorance.
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.
I think I would go a little further than that. There was a point in the past where machine architectures were so alien that even getting your favourite language bootstrapped on it was a major challenge.
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.
Praising C without acknowledging the 10 years in system programming languages and OS research done outside Bell Labs only shows an author's shallow ignorance in system programming history.
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.
First off, nobody's praising C. Second, those other developments - which I quite likely know better than you do so stop trying to show off or make an implied appeal to authority - are out of scope wrt an article that's specifically about C. I think it would be great if the world had converged on something like Mesa instead of C, but that didn't happen and it's not clear that it would have prevented misguided rants like the OP from being written. Whataboutism is not the same as understanding/respecting essential context.
Most of the problems exposed here aren't C related, but OS or architecture related. An ABI where function parameters are passed over A, B, and C registers and the return value is stored in the Z register have nothing to do with any language.
Except that C defines the way nearly everything everywhere passes function parameters over registers. Not C's fault, but still a thing affecting everyone that doesn't write C, as C doesn't even try to be a good ABI, because it wasn't meant to be.
What does this mean? C defines the mechanism(s) for passing arguments to a function? That's news to me. The parent's point was that the binary mechanisms are OS- and architecture-dependent. (And compiler-/build-dependent too; e.g., when compilers are designed to pass arguments via registers rather than on a stack.) As other commenters have tried to point out, the diversity of target platforms precludes a single, one-size-fits-all ABI -- for C or for any other language.
How does a language "try to be a good ABI"? An example of such a language would help.
C itself doesn't, but the OS deciding how it interfaces with C quickly results in all C programs for that OS doing the same thing, and, since C is the lingua franca of programming languages, in every single program with a cross-language API.
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
> Except that C defines the way nearly everything everywhere passes function parameters over registers.
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.
> My problem is that C was elevated to a role of prestige and power, its reign so absolute and eternal that it has completely distorted the way we speak to each other.
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.
Funny you should mention entropy. C is indeed a high-entropy programming language, and one indeed should prefer a language with fewer ways of doing or expressing the same thing.
Yes, for the most part intmax_t (and similar guarantees) is considered a mistake.
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).
Imagine if your language had to speak several other languages instead of variations of C implementations to work on different systems. Now that would be even more chaos potentially.
I sympathize with the authors pain and understand what they are striving for though I also don't have clear cut solution in mind. A lot of blood sweat and tears are expelled for compatibility amongst things that for the most part are not that important or interesting, because they are a trivial data transformation. The choice of calling convention, endianness, data layout. Imagine if an engineer's design could only work if a specific thread handedness was specified for each screw. It feels like a problem that could be solved with something better, but there's a "leaky" aspect that has to be addressed. Sometimes a uintX is a number with ordinal and/or arithmetic properties and sometimes it's a bit field where every bit has a semantic meaning.
How should
struct foo { int baz; float goo}; be laid out? What is the problem with the C abi that is fundamentally broken? Why shouldn’t args be passed on registers where possible?
This feels exactly like why Microsoft (and others) during the 90s started to define a well-defined subset of C, with some fancy IDL stuff, and a C-like ABI where all the C stuff needed could also be generated from the IDL, as standard somewhat-object-oriented interfaces like COM and other DCE RPC-likes.
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.
If you reach a bit over into IPC/RPC, rather than just linking, the options with decent IDL and cross language code generators start to increase quickly. For example D-bus, or androids binder. Some even use grpc or thrift between processes on same machine. All of these are of course operating one or two abstraction levels up but are still good to consider.
...You've just discovered why interfaces like COM and Cocoa are a good idea, yes. Welcome to 1999. Join me in cursing the proliferation of POSIX and FFIs. JOIN ME.
Not exactly. They use a different calling convention (for systems where 'this' pointers are normally passed in a special place) from usual C++ classes to match C-ish, and there's a few other restrictions for things (overloads, inheritance) that otherwise differ across C++ ABIs and wouldn't map well to other non-C++ languages.
The C wrappers generated by MIDL are also fairly similar to raw C++, compare (where pUnk is an IUnk *):
COM can also be seen as a mapping of C++ APIs (with some extras like interface versioning) onto C APIs, mainly as workaround for C++'s missing ABI stability (so that C++ APIs can be exposed via DLLs in a (C++) compiler-agnostic way).
348 comments
[ 2.9 ms ] story [ 309 ms ] threadYou 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.
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?
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.
https://news.ycombinator.com/item?id=17165179
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.
[1] https://go.dev/doc/go1.16#openbsd
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.
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.
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.
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:
and: >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?!
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.
[1] https://github.com/titzer/student-projects
The author is clearly and abundantly aware of this, and it isn't her complaint. Did you read the article?
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.
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.
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.
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.
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.
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).
What’s the problem with the ABI?
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.
The C wrappers generated by MIDL are also fairly similar to raw C++, compare (where pUnk is an IUnk *):
with... which expands to something like this: