back
326 comments
C's most infamous parsing difficulty is:

    A * B;
Is it A multiplied by B, or B declared as a pointer to A? It can't be resolved without a symbol table. But there's another way that works very well: it's a declaration. The reason is simple. The multiply has no purpose, and so nobody would write that. C doesn't have metaprogramming, so it won't be generating such code as an edge case (unless using the preprocessor for metaprogramming, in which case you deserve what you get).

But there's a worse problem:

    (A) - B
Is it A minus B, or casting negative B to type A? There's just no way to know without a symbol table. One might think who would write code that has a vacuous set of parentheses around an identifier? It turns out they don't, but they write macros that parenthesize the arguments, and the preprocessed result has those vacuous parentheses.

D resolves both issues with:

1. if it parses like a declaration, it's a declaration

2. a cast expression is preceded by the keyword `cast`

and D is easy to parse without a symbol table.

I don't see why resolution without a "symbol table" is a big deal. Knowing whether A is a type or not resolves these fairly easily. And in well written code, it should generally be obvious whether A is a type or not so it should not be a readability problem either.
> But there's another way that works very well: it's a declaration. The reason is simple. The multiply has no purpose, and so nobody would write that.

But there's another way that works very well: it's a multiplication. The reason is simple. A declaration of a variable that isn’t used has no purpose, and so nobody would write that.

As I think you know, C doesn’t handle this case by guessing that it must be a declaration. Its lexer looks in the tables that its parser creates to check whether a type called ‘A’ is in scope (https://stackoverflow.com/questions/41331871/how-c-c-parser-...)

> The multiply has no purpose, and so nobody would write that.

A fellow named Bjarne Stroustrup fixed that bug, though. In the plus plus dialect of C, A * B could reboot your system, without any #define macros for A or B.

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

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.

And they say romance is dead lol
Anyway to build a GUI app?

Definitely looks like a cool project. Can you write an OS in it, for microcontrollers ?

Are you planning to do an arm64-darwin port for Virgil?
This article is hard to parse because it doesn't present any alternatives that the author considers better.

- You have to speak the C ABI to talk to the OS -- yes, if you want an OS to support more than one language, or you want multiple languages within a single process, you need a common ABI of some sort. What is the alternative that doesn't involve a common ABI?

- C has too many ABIs -- yes, there are a lot of different kinds of hardware, and sometimes multiple software ecosystems evolved in parallel on the same hardware (eg. Windows and Linux). What is the alternative?

- ABI changes are hard to make in a non-breaking way -- yes they are. It's fundamentally a hard problem. You could compile everything from source every time, but the open-source world seems to have decided that there isn't enough time or CPU power to "live at head" and build from source every time, like Google does: https://abseil.io/about/philosophy#we-recommend-that-you-cho... (Disclosure: I work at Google).

- "You Can’t Actually Parse A C Header" -- this is, IMO, the most actionable objection that the article raises. You could imagine a subset of C that does away with macros, typedefs, etc. in order to be easier to parse, and in doing so forms a more accessible ABI specification language. That seems eminently doable. But parsing is only part of the overall battle: actually implementing the ABI for 176 triples seems like the bigger problem. Especially when you add the ABI-impacting function attributes mentioned in the tweet.

As a C fan, it's hard to see the language itself blamed for what are (in my view) just fundamentally difficult problems.

I don't think the author blames it on C. C didn't choose to become the ABI of 99% of the world. But yet, it is.

> You have to speak the C ABI to talk to the OS [...] What is your alternative that doesn't involve a common ABI?

Emphasis is on the _C_ ABI. The alternative is something that at least tries to be designed as a universal ABI.

> there are a lot of different kinds of hardware, and sometimes multiple software ecosystems evolved in parallel on the same hardware

If there was an explicit ABI standard though, evolving ecosystems would have to go through the standardization and thus you just couldn't get conflicting ABIs for the same thing.

If such standardization could cut the number of ABIs in half, it'd still be a huge win. And I think it'd be possible to go much further than that. Furthermore, it may be possible to split orthogonal things apart, such that instead of e.g. 50 targets, you have 5 OS targets and 10 arch targets, each specifying a separate part of the ABI. (current ABIs obviously do this to some extent, but not in any specified & structured way)

> As a C fan, it's hard to see the language itself blamed for what are (in my view) just fundamentally difficult problems.

I also like C. I agree these are hard problems. But I don't think they're problems that C should be dealing with. But we're pretty much just stuck with C.

> "You Can’t Actually Parse A C Header" -- this is, IMO, the most actionable objection that the article raises.

You don't need to parse C headers in order to use the same ABI. You can read DWARF info which is designed to convey type definitions, function/method prototypes etc. in a machine-readable way. It's already there, no need for a separate "IDL" standard at all.

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.

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.

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.

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

If I was wrangling on FFI issues across multiple platforms and battling gratuitous incompatibilities, bad documentation and bugs, I would be ranting as much or more as the author.

The issue is that there isn't really an alternative. Obviously a cross platform ABI is never going to exist (different endianness, alignment, register usage, stack conventions just considering the CPU, OSs themselves add more complexity). We could have an IDL to describe in details the ABI, but then:

- you need all OS vendors to be on board, which is not going to happen.

- even if they did you can be sure it will be forked in a myriad of dialects and non conforming implementations.

- even if everybody plays ball, bugs will still happen.

The best next is for language designers to come up with community maintained IDLs and tooling to interface with various languages instead of waiting for platform vendors to provide them. This is a realistic solution that can work, but at this point you might just accept that C fulfils this role already even if it is far from ideal. Just embrace libclang and hold your nose.

edit: there is also the option of targeting a single virtualized platform like the JVM or CLR which is great, but not really appropriate for a system language.

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

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.

A lot of smoke here, but very little light.

What this article fails to capture is the fundamental question that is faced any time a new architecture is encountered: should 'int' be sized (number of bits) according to its original (or most recent) de-facto definition? Or should it be sized according to the natural register size of the architecture's general-purpose registers?

A lot of us went through this back in the mid-2000's when AMD64 (x86_64) came out. It took both Microsoft and the Linux crowd (just to name two communities) time to come up with their respective (and incompatible) translaitons of types. The top answer to this StackOverflow question summarizes this well:

https://stackoverflow.com/questions/384502/what-is-the-bit-s...

In the gaming industry, we came up with our own typedefs until the standards caught up. Things like int_8, int_64, etc. I say only a fool would ever use somehting as pretentious a concept as 'intmax_t' in normal code (which isn't a tranlation table of typedefs based on platform). "max" according to whom? That is not the way.

None of this is C. All of this is the OS.

Who specifies how you talk to the OS, and how native applications talk to each other? The OS does. And it does, in fact, differ across different OSes (with different calling conventions). The only reason C comes into this picture is because it runs on all the platforms these other languages do (and many more), so you can write an adapter between the language and C, and not have to worry about supporting 10 million different calling conventions, because some C compiler author has done that for you.

So the article is half right; this isn't a programming language. But C sure is.

(And that's not to mention the fact that to some extent the ABI and calling convention is determined more by the CPU architecture than the OS, much less the language!)

C descriptions of the OS interface API...

Ok - I get that. But, there is a system call interface. int 0x80, syscall.

Now, these can be wrapped, and the result exposed in a completely different way. But that is the definition. If its "C" on one side and "C" on the other... well, ok then! I though Linux had vDSOs to allow direct "ABI" calls for performance reasons.... utilizing that will force a certain "C-ish" look. In turn, that can be wrapped. None of this changes quickly.

Heck. CP/M-80 had "CALL 5" with registers a certain way. Wasn't "C" by any stretch!

Because the C (POSIX, mostly) "API" is stable and available, we tend to use it. Wasn't always the case -- after all, FORTRAN I/O was all the rage back in the 60s (cf SNOBOL4).

If (whatever) programming system wants to avail itself of the C infrastructure, it is certainly free to do so. Stop the endless whinging about C! Why C? It is the only language in its class that works from Z80 to my Thinkpad.

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.

What we need is an ABI-level IDL [1] to specify library interfaces, that every programming language could use for imports and would translate to implicit declarations, both for calls to the interface and/or for implementing the interface. Currently, C header files poorly serve that purpose, and at the same time, that use impedes the evolution of C itself. Such an IDL wouldn't necessarily solve C's own compatibility issues with its evolution, but it could dramatically improve the FFI situation if widely adopted.

[1] https://en.wikipedia.org/wiki/Interface_description_language

"there are languages people complain about, and languages nobody use" (quote from the creator of C++)

It's easy to complain about C or C++, but I don't see that many languages that compiles to machine code, and offer compilers for many platforms.

I agree that C is somehow the new "assembly".

C is the glue used for building operating system, so it's not surprising you need to use it in many places.

I'm sorry but I don't see any better language that is as simple to learn for students as C, and fixes some problems of C. Most new languages are often difficult to read, introduce a lot of un-needed sophisticated features, and are not that much used because they're too niche.

A good example is Rust. Sure, the safety is an awesome feature, but ADA already did it somehow, but it's not what developers need.

There is a reason the world talk english and not german or japanese. It's because english is just much, much easier to learn. Nobody cares if the language makes sense.

It's odd because HTML and javascript are so much more ambiguous and cause a lot of pain, yet I hear more complaints about C or C++.

...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.
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.
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 feel like there has been a lot of energy put into creating system programming languages that fix the shortcoming of C and C++. But it seems to me that the solution is to just realize that we rely on C too much in code bases, to talk to OSes, and between programming languages to ever get rid of it. So instead of trying to replace C in all of those cases, just make a better C. Simplify the language to the point where parsing it is almost trivial. Barring all of the above, why not just create a new standard where people agree that

A * B; And (A) - B Can only mean one thing. Sure this wouldn’t do anything for the old code, but at least where new C code was required we wouldn’t continue dealing with all of its complexity.

I feel like this is the same problem with CSVs. It is trivial to parse if people follow the standard but most people don’t. But if you create a parser that only accepts the standard and refuses to parse non standard CSVs then you never have to deal with any of the BS edge cases that come up. And if you show where in the code/CSV the ambiguity is, people might actually fix their code to get it to parse correctly instead of leaving it ambiguous

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.
> Now C isn’t just a programming language, it’s a protocol.

But the image above that is calling conventions for functions on a particular processor. What does that have to do with C?

> So actually semantically parsing a C header is a horrible nightmare

I'm confused. It seems to me the right way to go would be to improve C such that it's more precisely specified, easier to parse, etc. Genuine question: why is that so hard? (I suspect the reasons are more institutional than technical.)

Variable declarations are syntactically weird in C, for example. And newer languages seem to have better ways (that are actually context-free). So why couldn't C be evolved toward that?

> int foo(int x, int y)

becomes

> foo(x: i32, y: i32) -> i32

and so on.

> You don’t see England trying to improve itself, do you?

I guess that's a saying I'm not familiar with. Seems to me England has improved a lot over the years.

Go easy on me, geniuses.

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 of this is not really C-specific, really. The ABI issues are likely to arise in any systems programming language, and in any lingua franca between languages.

These things come up in C first because C is the lingua franca.

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.
Ah, the `intmax_t` problem is a fun one. Perhaps it should never have been introduced.
> 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.

WebAssembly (wasm) will save us all! https://hacks.mozilla.org/2019/08/webassembly-interface-type...

Who knows, in the future maybe wasm will be the common ABI between languages.

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.
In the same vein, my favorite troll on this topic is "The C language is purely functional" (2009) from Haskell programmer Conal Elliott's blog: http://conal.net/blog/posts/the-c-language-is-purely-functio....
> 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.

Sure they can, you just need to build the mechanism that allows them to.

>hear that everything on Linux is “just a file”, so let’s open a file on Linux!

That man page is for glibc. You instead want to look at man syscalls and man syscall to look up how to interface with he kernel. Linux does not require programs to use any amount of C.

Well, I can tell it's a programmer and not an engineer who works at hardware levels. All the complaints are "1st world" or "high level" programmer/computer problems.

If you've ever designed hardware and had to bootstrap it up to a "high level" situation - then these are nonsense concerned when you are merely trying to get a new hardware design up and running. You can always bootstrap to a language that already has all these things. The author obviously has never done anything like that. Not based in any reality where C matters.

C is still a programming language, even if you are frustrated with its ecosystem/standard.
Everyone's here talking about ambiguous symbols but anecdotally speaking I've maybe run into this problem once or twice in roughly 10 years.

Hardly an argument, in my opinion, and it's annoying to see parroted on every thread about C's syntax.

Any programmer can use any programming language that he wants so long as it is C.

Henry Ford

The D compiler has a full C compiler in it for parsing headers.

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.