Not really. C won because it was the standard compiler for Unix, and was a free compiler for a free operating system in a time when both were highly unusual.
But in the past few decades, C has lost a lot of its market share to other programming languages. In the realm of desktop (and mobile) applications, C is basically unused for new projects--its standard library is truly anemic here, and major support libraries (e.g., GUI toolkits) are often in C++ and not C. Where C is still the dominant language is the land of embedded applications, and it's not had a lot of competition here since most languages don't bother trying to define a freestanding implementation.
Rust is really the first language to try to contend this space. And there are signs that it may supplant C: Intel is apparently looking to move its firmware to Rust; Linux is allowing Rust for device drivers and kernel modules. Hell, even some OS programming courses (e.g., Stanford, Georgia Tech) have moved their curriculum to use Rust instead of C.
In addition operating systems gives you bunch of safety measures if you design around them. (Obvious being separate processes)
One has to also remember that even if your program would be memory safe it still does not guarantee its flawless nor secure. One very good recent personal experience I can give you is certain popular swift SQL library. It was written in swift which also boasts the "memory safe by default". However, that library due to its design was subject to the very basic SQL injection attacks. Needless to say I ended up writing my own SQL bindings. I hope that library is now fixed or something replaced it, as since at the time I had to write swift (and no I didn't like it), it was the only and most popular (scary) option on github.
I could also write long rant about threads and how the implementations and the threading primitives often have subtle bugs or are broken in different ways. And how using threads in general makes your application basically undefined unless you only absolutely know every piece of code that runs on that thread.
To put it in short, C programmers hate needless abstraction and work with raw memory and hand-crafted data structures whenever possible. More data, less code.
I couldn't afford abstractions, memory safety guarantees, algebraic data types, generics, pattern matching, thread data safety (at least in the context of interrupts). The languages that had these things were hulking languages with giant runtimes and exactly zero support for embedded development. Not to mention no vendor toolchains.
Rust supports all these things, with no allocator, no standard library, and often with zero additional cost -- in terms of compute and in terms of memory. Then, being built on LLVM means that the vendor toolchain support is quickly becoming a non-issue. I suspect we'll start to see more and more of Rust in embedded, but only time will tell.
And as others have called out, Rust has very few OOP features like an optional notion of a `self` on a function bound to a structure. There's no classes, no subclassing, no message passing, no inheritance at all (structure, interface or implementation), limited dynamic dispatch, no polymorphism.
As an embedded developer for a living, if you put it that way, then the first thing it comes to my mind is "so why should I bother learning Rust" for embedded.
Other than the usual "please consider UB and network/memory handling/security issues" (reasons that don't really affect me), so far nobody could provide a convincing answer.
It's not that the usual answer I get is wrong or invalid or doesn't have a point. But if I ask "ok, what else?" there is really little motivation for me to move on.
Someone once told me I'll become an outdated curmudgeon here on HN, but I think I'll be long gone before something truly deserves to replace C for embedded (and low level).
> Someone once told me I'll become an outdated curmudgeon here on HN, but I think I'll be long gone before something truly deserves to replace C for embedded (and low level).
You (and I) may become an outdated curmudgeon before long, but it won't be because you refused to learn Rust haha.
Generics alone are a huge time saver compared to C, even if you use hacks like macros in the latter to do something similar already.
Embedded developers use C because they have to, not typically because they want to. And they have to use C because of proprietary toolchains and existing libraries, among other similar reasons. Good reasons, sure, but not because C the language is so great.
> hand-crafted data structures [...] More data, less code
You're paying lip service to the principle of datastructures-over-algorithms, yet you're advocating for a language which has neither algebraic data types nor tuples? Come on. That's like praising functional programming but using Fortran.
I wrote Monocypher in C for one reason: portability.
From a systems point of view, crypto libraries are trivial: you don't need any dependency, code is pathologically straight-line, there is no almost data structure to speak of beyond arrays of bytes and arrays of words.
Yet I can tell you that if not for portability, Rust would have been a better fit. So I could group buffers and their size in a single argument. So I could provide genuinely high-level interface. So I could use types to avoid silly mistakes and enforce some invariants. Portability won over all that goodness: worst case I can have a Rust wrapper. Heck, someone else already wrote one for me.
This. Even if it is less safe.
Because Rust is redundant, its safety guarantees are a subset of safety guarantees of ATS[1]. A project in plain C integrates more naturally with it at any point of its development cycle[2], and it doesn't require giving up safe pointer arithmetics[3]
[1] http://ats-lang.sourceforge.net/DOCUMENT/ATS2FUNCRASH/HTML/c...
[2] http://ats-lang.sourceforge.net/DOCUMENT/INT2PROGINATS/HTML/...
[3] http://ats-lang.sourceforge.net/DOCUMENT/INT2PROGINATS/HTML/...
EDIT: those who downove, let's discuss the topic in substance and let's avoid zealotry. If you promote and pitch Rust to the audience of C by advertising its safety guarantees, zero cost abstractions, and how well it integrates with C ABI, at least be consistent when it turns out that there's another $TECH that does it safer and more consistently with C programmers' reliance on certain useful features of C.
What's the point of waiting for 1.0.0? It's just a tag that doesn't save you from bugs and breaking changes.
What is industry support? What does it have to do with a team where everyone can read the documentation of the tool that is already built on top of a mature GCC ecosystem and that adheres existing approaches to debugging, profiling, releasing and maintaining C codebases?
> and doesn't use a modern build system or package
Why do I need a separate solution such as Cargo if I can build, package, and distribute everything with Nix and get reproducibility, distributed builds, transparent caching, and environment isolation along the way for free?
So I'd amend this to: C won because it was the only cross-platform low level language that was competently implemented for DOS.
That is a patently false statement. To understand low-level programming concepts, you need to understand fundamental notions about how machines represent state in registers and memory, how memory is organized (including primarily the concept of function calls and the stack), and the indirect referencing of memory via pointers. Note that nowhere in that list did I describe a concept that is unique to C.
In fact, one of the more common approaches to introducing developers to low-level programming is to introduce them to these concepts via assembly (say, Nand2tetris). In my own experience TA'ing such a course, I am more than willing to translate code into whatever language the student is most comfortable with to express the concepts as necessary. You can absolutely learn these concepts in other languages, and my own suspicion is that unsafe Rust does a slightly better job of it than regular C does.
C does not have a monopoly on understanding the low-level organization of code, and quite frankly, C's lack of coverage here can be frustrating. C has no concept of multiple return values, functions with multiple entry points, unwinding the stack, computed goto, SIMD vector types, nested functions, discontinuous structures, or tail calls, and these are all concepts that are present in other languages that cannot be expressed in standard C or often even in vendor-extended C.
> C has no concept of multiple return values, functions with multiple entry points, unwinding the stack, computed goto, SIMD vector types, nested functions, discontinuous structures, or tail calls, and these are all concepts that are present in other languages that cannot be expressed in standard C or often even in vendor-extended C.
ATS does a better job than safe Rust and you can think of it as a vendor-extended C (http://ats-lang.sourceforge.net/DOCUMENT/INT2PROGINATS/HTML/...).
Dunno how often this class runs, and if it was just for one semester or more than that.
Oh, did this happen? I remember some discussion some months ago, has it actually been merged?
Operating systems are currently written in C, and therefore have a C interface. Which language is best at interfacing with C? (No trick here, just a rhetorical question.) C of course. Other languages would need some sort of FFI, which is generally unwieldy enough that the designers hid it behind a comprehensive standard library.
C doesn't need an huge library to be available, you say? Oh but it does. It's called the kernel. Comes with a freaking huge runtime too.
> Imagine if you need to write a new OS, a compiler, a linker, or a shell interpreter... you get the idea.
I think I do, but I'm afraid you don't. Writing an OS (and the rest) in Pascal (I'm thinking of Oberon specifically) is no harder than to write it in C. If you write your whole OS in Blub, interfacing with Blub will be easiest in Blub, you won't need an extensive Blub standard library because you already have the kernel, all the tools (debuggers, editors…) will be Blub friendly…
Lisp machines used to be a thing, you know.
> My conclusion is that language designers still didn't get what made C so successful […]
Language designers can't even address what makes C so successful: network effects.
Not true. Operating systems are written in C with some assembly, and the interface to userspace is universally written in assembly. E.g., https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux...
Which by some eery coincidence happens to conform to the C ABI of the platform. Come on.
I stand by that claim: the only reason operating systems have a C interface is because they are (at least originally) written in C. The fact that the kernel has some parts in assembly is immaterial, even if those parts happen to comprise the userland interface.
And it's not just the kernel: when UNIX was re-written in C, everything was written in C. The compiler, the core utilities, the editor, the shell… the whole OS, not just the kernel. Of course it was easier to interface to that in the same language everything else was written in.
Likewise, the Oberon operating system, which was written almost entirely in the Oberon language (which should have been named Pascal-3) has an… Oberon interface. Want to use C to interface with it? My, you'd have to write a whole compiler, perhaps a non-trivial runtime, debugging tools, and of course an FFI to interface to Oberon, the de-facto linga franca.
C looks much worse when it's not already the king of the hill. Its strength lies in its ubiquity more than in any quality of the language itself.
PS. I appreciate you reaching out and making an effort to understand why the comment seems snarky to me.
You can't because it is impossible to escape the type system. Without the ability to cast pointers you can't write a memory allocator. You can't write a function like dlopen()...
Not sure about the memory allocator, Oberon may have avoided the problem by having a GC.
http://www.cs.virginia.edu/~evans/cs655/readings/bwk-on-pasc...
Here's what low-level programming in Pascal looked like on DOS: http://www.retroarchive.org/swag/TSR/0022.PAS.html
it did well enough that C stdlibs (at least MSVC's, LLVM's) and compilers (... pretty much all the big ones) are implemented in C++ and just export C symbols nowadays, likewise for newer OSes like Fuschia.
SerenityOS (https://github.com/SerenityOS/serenity) was written from scratch in two years in C++ and goes as far as having a custom web browser & JS engine. Where is the equivalent in C ? Where are the C web browsers, C office suites, C Godot/Unity/Unreal-like game engines ? Why is Arduino being programmed in C++ and not C ?
Objective-C, Java, Swift, and C# have become massively successful as application programming languages because C is/was terrible at it. They learned a lot about how painful it was to do basic higher level programming tasks when you are restricted to C's semantics and memory model.
C is great but I don't think it's worth romanticizing since history has shown that C isn't that great for writing anything but systems code. Which is a restricted domain to begin with, and isn't even that attractive for it anymore.
The one thing C has over anything else is interop. The language of FFI is C. There's no inherent reason for that other than history, and it's not super broken so we're not going to fix it.
C sucks when you need convenience of big standard library or safety above performance. There is nothing like C when you care about speed, memory footprint and efficient memory management. It's great other languages took over in areas C is terrible at but it's not like areas when it's the best and often they only option disappeared.
You can absolutely beat C in everything you list. And you also can't. I don't agree that C is the end all be all of performance or code size, except in a handful of cases where there's nothing else available.
C started to be popular in the microcomputer world at a time when systems programming was being done in assembly languages. For instance, most arcade games for 8 bit microcomputers, were written in assembler. Some applications for the IBM PC were written in assembler, such as WordPerfect.
The freedom with pointers thanks to arithmetic would instantly make sense and appeal to assembly people, who would find a systems language without pointer flexibility to be too strait-jacketed.
Just _look_ at all these short keywords and special symbols. It's legitimately hard to read without focusing on each character.
#[bla(foo)]
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
println!("x is {} and y is {}", x, y);
}
impl<'a> Default for Borrowed<'a> {
fn default() -> Self {
Self {
x: &10,
}
}
}
This old fart thinks Rust is the new Perl.Here's a representative example of the code I wrote. This bit is outputting stuff to a file in some binary format: https://github.com/ValveSoftware/Proton/blob/proton_5.13/med...
E: If you want to take another stab, the way I learned Rust was the book: https://doc.rust-lang.org/book/ Actually type out every code example, it will help your fingers learn the "feel" of the language, and give you an opportunity to break things on purpose to test your understanding. Learning something new is always a challenge, but I really do like Rust and think it's worth the trouble.
[1] https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
E: There are only three instances of using explicit lifetimes in the entirety of the project I linked, if you want to see some real-world examples of it. In all of these, it is used to indicate that the struct being declared references another struct which must outlive that struct. That way we don't end up with a dangling reference if the referenced struct failed to outlive this struct.
https://github.com/ValveSoftware/Proton/blob/proton_5.13/med...
https://github.com/ValveSoftware/Proton/blob/proton_5.13/med...
https://github.com/ValveSoftware/Proton/blob/proton_5.13/med...
fn print_refs(x: &i32, y: &i32) {
println!("x is {} and y is {}", x, y);
}
In general you only need to specify lifetime parameters when there's an ambiguous situation. For instance, the following builds and runs just fine. fn make_substring_of(input: &str) -> &str {
&input[1 .. ]
}
I'll be the first to admit it takes a while to ramp up into Rust. Part of that is learning to let go and trust the compiler. Unlike many other languages, it won't hurt you haha. It's on your side. Sometimes you need to you know, give it a little more info so it can do it's job.The Rust team has made huge strides in improving writability of the language, especially with non-lexical lifetimes.
Soon, `fn` and `impl` and `Vec` fade into the background, like `char` and `short` and `long`.
Over the long term, I don't think symbols are harder to read than keywords. C itself provides some evidence for that: imagine reading C code if experience with another language had conditioned you to look past * and &. That would be terribly confusing, but for an experienced C programmer, those symbols leap out of the code at you because you know that they carry a lot of meaning.
#[] - why the [] if # already makes that line different from the usual code? seems superfluous
'a - is that thing next to 'a' a smudge on my display? Did I forget a quote? better wipe the display with my finger
'&a - "mut" and "ref" but ' ?
foo! - yelling out function calls. "print!" "exit!" "macro!". Angry Codes!
foo? - when you're done yelling, make sure to ask existential questions of the return result. This one I have the least problems with since it actually makes me question the return value ("hm, something's weird here, it could be null, pay attention"), but when coupled with the yelling, just makes the whole thing look dramatic. "do_it_now!(); did_we()?"
fn - by itself not a huge deal, but the list of truncated words that are used frequently is "impl", "mut" and "pub". I save some characters (am I really in such a rush?) at the cost of reading this broken English "f-n impl moot pahb". At least C doesn't have that. Pascal had "interface" "implementation", "begin", "end", etc. Java has "public", "interface", "extends", "class".
Default for Borrowed - suddenly English! No time to type 'function' or 'mutable' but "Default for Borrowed" is a-ok?
underscores all over the place in the standard library. Even C doesn't have that many, mostly in the _r variants that were added.
unwrap() - what does that word have to do with errors? (I know what it does) It wouldn't be my first choice.
I have a whole list of awesome stuff, meh-stuff and wtf-stuff I noted down about Rust while trying to learn it (on multiple occasions), and there are a lot of excellent things about Rust, but the aesthetics of the language are important, otherwise we'd all have no issues coding in Brainf*ck.
Yes it's possible to go nuts with syntax, but this just feels like the "programmer art" of language syntax. I think it's usable (clearly), it's just not elegant to me.
int and char aren't exactly words. And let's not forget that a type like "double" makes no sense whatsoever by itself. (It's two of something, but two of what? Oh, it's "double-precision" floating point! How could I miss that?)
Now, let's look at C's standard library:
strcmp, strpbrk, isalnum, ispunct, setjmp, SIGSEGV, SIGFPE (that's the divide-by-zero exception, isn't it obvious?), SIGABRT
Sure seems like C has its own massive issue with "we can't let a name be long"
> underscores all over the place in the standard library. Even C doesn't have that many, mostly in the _r variants that were added.
va_arg, FE_DFL_ENV, int32_t, etc. Continue on into most C libraries, and underscores are pretty common because C has no other namespacing mechanism.
Syntax is pretty strange in any language if you're not used to it. If you're comfortable with C, C's syntax and spelling quirks don't stand out to you.
I personally have higher expectations of a language designed in the 2000s by people standing on the shoulders of 40 years of computer science and language research.
Here's a contrived analogy:
Imagine if you bought the newest Tesla truck meant to replace old Ford model trucks and you had to vigorously shake the steering wheel in order to lower the driver's window.
You're saying: "Ford trucks have always used a hand crank and that feels strange too if you're not used to it!"
I'm saying: "Why did they choose to make it equally strange in the first place?"
Ahh yes, I believe that came about due to the great "Vowel Bowl" of the 1970s. There was a serious shortage of vowels.
Since the first edition of ANSI/ISO C was trying to codify the already-existing common practices for maximum portability, it reflects those existing limits (5.2.4.1 "Translation limits"):
"The implementation shall be able to translate and execute at least one program that contains at least one instance of every one of the following limits:
...
31 significant initial characters in an internal identifier or a macro name
6 significant initial characters in an external identifier"
If you look at many abbreviated function names from the C stdlib, they are specifically 6 characters long - strcpy etc. I wouldn't be surprised if the 6-char external identifier limit goes all the way back to the first K&R C compilers.
> #[] - why the [] if # already makes that line different from the usual code? seems superfluous
# does not "make that line different from the usual code." The whole #[] construct is it, it has nothing to do with lines. You could put "#[foo] #[bar]" on one line if you wanted, you could write "#[foo] fn lol()" if you wanted...
> foo! - yelling out function calls. "print!" "exit!" "macro!". Angry Codes!
This helps both humans and computers parse; macro invocations don't have to follow regular Rust syntax, and the ! helps indicate that that's true.
> Default for Borrowed -
This is not language syntax, this is the name of two types. You can name your types however you'd like.
I don't even know what #[bla(foo)] does, or why all that punctuation is needed. maybe nesting is allowed?
> 'a - is that thing next to 'a' a smudge on my display? Did I forget a quote? better wipe the display with my finger
this is a lifetime annotation, which is a genuinely noisy bit of syntax.
> foo! - yelling out function calls. "print!" "exit!" "macro!". Angry Codes!
I guess they really want to make sure you know when a macro is being used. probably a result of ptsd from debugging c and c++ code :)
> fn - by itself not a huge deal, but the list of truncated words that are used frequently is "impl", "mut" and "pub". I save some characters (am I really in such a rush?) at the cost of reading this broken English "f-n impl moot pahb". At least C doesn't have that.
the c keywords aren't too bad, but the standard library is full of this kind of thing. stdio.h and string.h immediately come to mind.
I'm not so much defending rust as I am pointing out that c's syntax isn't that great to begin with. I've been writing c and c++ code every day for several years now, so it's usually pretty easy for me to skim and understand what is going on. but if I try and place myself in the shoes of a newcomer, I don't think the syntax is much better than rust. remember the first time you tried to parse the type of a nontrivial function pointer?
People proposing every other language that tried to replace C thought the same. Only time will tell, of course, but I wouldn't bet on it. Nowadays C++ is seen by many as a "garbage pile", the same can happen to rust.
Did you mean API? The C++ ABI situation is famously unstable.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p186...
"there is a non-trivial amount of performance that we cannot recoup because of ABI concerns. We cannot remove runtime overhead involved in passing unique_ptr by value, nor can we change std::hash or class layout for unordered_map , without forcing a recompile everywhere etc. etc."
I thought passing template instantiations over an ABI boundary was generally discouraged and thought to be asking for trouble. I guess this isn't really at odds with what the paper is saying though - it could still be that a lot of people are doing so.
edit Thinking about Hyrum's Law, [0] mentioned in the article, makes me think perhaps there was an upside to Java firmly refusing to support any kind of ahead-of-time compilation for so long. It fully closed the door on any funny business distributing Java packages as brittle precompiled native-code blobs, ensuring the bytecode format remained the way that Java packages were distributed, presumably avoiding some fraction of the issues C++ now faces.
Of course, Java still has backward-compatibility obligations, but unlike in C++ they align pretty well with API compatibility, if I understand things correctly.
is a couple ABI breaks for std::string between C++98 and C++20 that unstable ? You can write code that uses std::string today and links against a .so built a long time ago (modulo compiler bugs of course, thus the various versions here: https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Optio...). GCC & libstdc++ go to great lengths to preserve ABI compatibility.
C++ was seen as a garbage pile from its inception by a large number of people.
A significant number of people kept using C because the only alternative was C++ and that wasn't acceptable.
I am sure it has its place but I think it's just too ugly to be attractive. There is something pleasing about writing C, Python or even Javascript which you will never get with Rust. It will never be a language a lot of people enjoy writing imo.
C++ too to a lesser extent. I work on spacecraft flight software, and there's a significant push to move from pure C to modern C++.
No single language is going to (or wants to IMO) replace C in every single use case, but replacing C in specific use cases has been a huge boon for productivity.
Otherwise, I can't think of any circumstance Rust isn't trying to muscle in on C and C++'s territory.
New languages mostly don't replace existing ones. Rather, they supplant them for some uses, and open up new kinds of software which are easier to implement or to conceive with the new language. Now, you referred to C++ specifically, and since I'm somewhat familiar with it I'll address some of the points you made with respect to just that one:
C++ was not intended to "replace C", but rather to combine features of BCPL (later C) and Simula. See: https://www.youtube.com/watch?v=69edOm889V4
Bjarne Strousup said: "If you want to create a new language, a new system - it's quite useful not to try to invent every wheel." For a long while now, C++ teachers/trainers encourage their audience _not_ to think of C++ as an "augmented C" or "C with feature X Y and Z", and to avoid most "C-style" code in favor of idioms appropriate to what the language offers today.
Also, C didn't "win the competition" because there isn't a "bestest language for everything" competition. It has been, and is, a popular language with many uses. Writing operating system kernels is one kind of programming task, where C is the most popular. Even at this level (and lower still), other languages are potentially interesting and often used. See, for example:
* IncludeOS - Running C++ without an operating system: https://www.youtube.com/watch?v=cQPrtTsM7Zg * Generating optimal assembly in an embedded setting at (C++) compile time: https://www.youtube.com/watch?v=CNw6Cz8Cb68
Finally, C++ doesn't require a huge library nor a complex runtime system because it has a "freestanding mode" in which the requirements are very limited (although more is required than for C). See: https://en.cppreference.com/w/cpp/freestanding
import core.stdc.stdio;
int main() {
printf("hello world!\n");
return 0;
}
and yes, it is calling C's printf. To build it: dmd -betterC hello.dImagine a world where most the languages follow Lisp's syntactic conventions, or Pascal's. What a nightmare.
var x: *int;
more often than: int* x;
i.e. type name follows variable name, and type modifiers work more like unary prefix operators on types. And this is because it's less ambiguous to parse, and makes more complex types a lot easier to read, since you simply go left-to-right, instead of C's "spiraling" declarations.The rest of Pascal's syntax is not particularly problematic, either. I'd say that the two biggest problems with it were begin..end for blocks, and having all local declarations in a block separate from code. But Modula-2 already dropped "begin", and various Pascal dialects added inline variable declarations eventually. So, on the whole, I think we'd actually be better off in terms of code readability if Modula-2 rather than C became the standard systems programming language.
Say you add fn and var as keywords. Exactly how hard would that be to fix? You could probably write a tool to do that.
The default is not the best, the default has just beaten the world so many times on the head that anything else became foreign and weird and got laughed out of the room before it even had the chance to say anything.
One programming language history book/blog/paper a day keeps the nonsense notions away, C "Won" like people win the lottery or the roulette.
Everyone of these language brought new ideas, but they don't stand a chance because their designers don't understand the point of C. The C language didn't win because it was the "best" language or had the best set of features. Far from it. Even in the mid 70s it was a backward language compared to other cool languages of the day like Algol and Lisp.
C won the competition because it just gives programmers the bare minimum functionality to put an operating system and a compiler in place! It is flexible, you can provide your own library if you want, and therefore gives your easy portability. OS writers will chose C any hour of the day or night because it makes their job easier.
By comparison, other languages will require a huge library to be available, and sometimes a complex runtime system, just for you to write a simple "hello world"! Imagine if you need to write a new OS, a compiler, a linker, or a shell interpreter... you get the idea.
My conclusion is that language designers still didn't get what made C so successful and therefore keep coming up with shiny complex things that don't stand a chance to become the next C.