The speaker discourages C for new projects, but that says more about the problem domains they work in than C itself. C is what it is because the hardware and assembly language are what they are. Folks who want a safer C should design a new hardware architecture with a "safe" assembly language and a new low-level language that targets it.
It's a massive red flag that they don't know enough to be useful.
C is great for the things C is great for, however small that range may or may not be now and in the future.
Any other stance is reductive and misleading.
I’ve been writing C since 1991 and I can’t think of anything where I wouldn’t start a new project in some other language. There are many interesting choices of varying maturity in the low-level systems programming space: Zig, Rust, Crystal, D, Swift (if the standard library ever gains support for system-level programming). Even the “better C” subset of C++ will allow you to avoid certain classes of security bugs completely.
I don’t think C is even merely adequate for anything at this point; defaulting to memory safety is table stakes in any domain where C was once dominant.
Rust is exciting and proving itself about as rapidly as such a language could be expected to do. But there are big questions around how all of this plays out in a long-lived project. There's a good argument that C's simplicity is an asset here.
I like Rust a lot and have done some cool stuff with it. I believe the challenges of long-lived projects will be solved. But at the same time, I admit that there are a lot of other factors (including non-trchnical ones). If you pick a language that doesn't last for whatever reason, and you have a couple decades worth of code, the options are grim.
Implementing low-level code that is important enough to prove correct, without going through the additional effort of de novo proving your entire toolchain is correct.
If I use a micro controller to control a string of leds I do not care about security/memory safety. But I do care about being as close to the metal as possible, and being able to understand the compiled code.
But many microcontrollers these days will likely have a TCP/IP stack, perhaps even crypto, even if it is to control a string of LEDs via MQTT or Modbus TCP.
Give you another example. A watch, not an apple watch, but a simple Casio watch. One that does time, alarm and a stopwatch. Not connected to anything. What is important here is the battery life, so the less code the better. C would be a fine choice here. All additional code to prevent security breaches would be a complete waste here.
Give another example. As my day-job I develop embedded software for the railways. Current system I work on operates the brakes when the train goes too fast. Not connected to the internet, and no connection would even be allowed. Written in C. One because it is simple to understand and the developer can focus on getting the functionality correct. Secondly because there is a wide choice of additional tooling and standards that is required to get the application certified.
Thankfully returns in digital stores, warranties in consulting projects, lawsuits in business losses and cybersecurity bills are slowly changing that.
There are plenty of things that are controlled by a micro controllers that do not have a network connection.
A micro controller hitting a memory safety bug that causes the LEDs to present invalid output can have real world consequences. Even if the micro controller isn't directly actuating machinery, it might cause an operator to incorrectly act because they were mislead.
If the micro controller is running Christmas lights, it might be fine if it falls over, no one will die, but I would still call that a manufacturing defect.
There.
Writing readable code. I'm a huge fan of Zig and Rust too but at the end of the day they just aren't C.
It's really not, though. What C is an abstraction of is computer architecture as it existed by the late 1960s.
I don't follow. Barring micro-code, hardware is designed for executing either RISC or CISC instructions. If anything, the industry has matured and we see less esoteric ISA's today than in the 1960s.
Would that be LLVM's IR or MLIR (https://mlir.llvm.org)?
It's almost the exact opposite extreme on the pendulum, where C allowed anything while Rust limits to only what the language designers conceive as proper and not just safe.
Zig can gain memory management systems like Nim's ARC which works well for system design and adds temporal safety. On the other hand Rust's trait system likely will never become an "open ended" type system like say Julia's. Heck, even overloaded function types don't seem likely in Rust.
Though, it looks like Zig doesn't do function overloading either [1]. That's a disappointment. So you end up with `array_count`, `map_count`, etc instead of just `count`. In my way of thinking that's more work reduces readability. It's one of the paint points of C vs C++ to need `array_list_count` and `hash_map_add` instead of just saying `vec.insert(...)`.
The biggest ones for me in Rust is that it disallows extending traits for types you don't own, and the lack of function overloading. Neither of those are required for the borrow checker or safety, but it's a philosophical design decision.
Another issue with function overloading is that it can make code more difficult to debug. If a bug is found in one of the overloaded functions, it can be difficult to determine which function is causing the issue. This can make it more time-consuming to fix the bug and can lead to frustration for the developer. I remember debugging an issue at OkCupid and we lost many hours due to debug information being collapsed for overloads, making it look like the wrong function was being called in the debugger.
Finally, function overloading can lead to code that is more prone to errors. When the same function name is used for multiple different purposes, it can be easy to accidentally call the wrong function with the wrong arguments, which can lead to unintended consequences or runtime errors.
In conclusion, good riddance. This is what makes Zig a great language, that it doesn't have garbage like function overloading.
This is a similar argument to Hungarian encoding, IMHO. A decent LSP makes it trivial to see which function is being called. The other issue you mention is a problem with the debugger/compiler, not function overloading.
> Even finding what file the function is in can be a non-trivial task.
Not really, control-click and you’re at the function def.
> it can be difficult to determine which function is causing the issue. This can make it more time-consuming to fix the bug and can lead to frustration for the developer.
Not any harder than “method” overloads, which it sounds like Zig does have.
Personally I find the opposite. Having 20 names for functions that all equate to `len` requires more mental overhead.
We're talking about replacing C here. Requiring everyone depend on a hefty LSP to make sense of source code is a big ask.
Source code is text. I firmly believe that all semantic information of a piece of source code should be expressed as text in said piece of code. I already see code where the language allows the programmer to omit types from variables for 'brevity', and the assumption then is that everyone working on that code is using a fancy enough text editor that can stick in extra labels to show the missing type information. Absolutely baffling to me how people find that acceptable in any way.
The orphan rules are definitely necessary for coherence: otherwise, you could end up with a situation where two different crates try to implement the same trait for the same type, and there would have to be some (likely unwieldy) mechanism to resolve that.
Also, it's not that you can't implement any traits for types you don't own, it's that you can't implement traits you don't own for types you don't own. So you can still, e.g., create your own extension trait and implement it for whatever type you want. (But you can't do that while also creating a blanket impl for types implementing the original trait, which is a bit of a pain.) And, of course, if you need an object to implement a trait you don't own, you can define a newtype wrapper over it, but that can also be difficult to work with sometimes.
Perhaps this situation could be improved by one of the "crate-local impl" proposals that have been floating around. I'm not entirely sure how those would interact with existing implementation from the defining crates.
I think Julia, D, Nim, and others show its possible and generally easy to work with open ended type systems. I think Haskell does as well?
Though yes those come at the cost of possible conflict or user confusion, which is why I consider it a philosophical decision. It matches with the decision to not allow user code to use trait specializations in stable despite the stdlib having it.
Imports generally seem fine for controlling what gets used. Want a trait impl, import it into a module. Cargo crate features might also be a route to enforce package level decisions.
> And, of course, if you need an object to implement a trait you don't own, you can define a newtype wrapper over it, but that can also be difficult to work with sometimes.
Unfortunately that means you can't define 'default' or 'clone' traits for a type. That prevents you from using derives on your newtypes as well. That means manually implementing clone, or serde which is a PITA.
> Perhaps this situation could be improved by one of the "crate-local impl" proposals that have been floating around.
At least that'd make it somewhat easier to work with. It'd still not let end users / programmers to mix and match types and traits from different libraries without a lot of unnecessary work.
Sorry, could you give an example of this? I can't find any way to extend existing types in those languages with some brief Googling.
> It matches with the decision to not allow user code to use trait specializations in stable despite the stdlib having it.
Keeping specialization unstable is much more a practical decision than a philosophical position: if they could, they would've stabilized it years ago. The problem is that specialization very quickly becomes unsound in combination with lifetimes. The compiler erases all lifetimes on types after checking them, since monomorphizing a new type for each lifetime would lead to an exponential explosion (this can't be changed at this point without redesigning the language). Therefore, users must not be able to specialize a trait impl on certain lifetime combinations (or certain lifetimes like 'static), since the compiler would have absolutely no way to tell which impl to use. And in turn, completely barring lifetime specialization becomes a daunting challenge with the existence of associated type projections and blanket impls. Specialization definitely isn't kept unstable just because they think users can't be trusted with it.
> Imports generally seem fine for controlling what gets used. Want a trait impl, import it into a module.
I don't think this would be compatible with blanket impls, since you couldn't just import every single potential impl in existence (and if you could, you'd run into conflicts). I suppose you could have a system of exporting impls, where to use a blanket impl you have to pass it another impl as input, but at that point you have new idiosyncratic system that would scare away users and would likely be far more noisy than a good newtype system.
> Unfortunately that means you can't define 'default' or 'clone' traits for a type. That prevents you from using derives on your newtypes as well. That means manually implementing clone, or serde which is a PITA.
If a foreign crate doesn't implement Default or Clone for its types, then how is the compiler supposed to derive it for your local newtypes? It can't just look into the foreign type's fields, if they aren't all public. Are you often having to work with fully public foreign types?
Overall, I get that the type system can be pretty frustrating as it stands today, but I don't see any better alternative than building better tools for defining newtypes.
Zig doesn't have function overloading but it does have namespaced functions, so you can define your types and your "methods" on them.
Rust does not have a defined standard yet, but I suspect it also would be quite large.
- Swift has plans to incorporate temporal safety in upcoming releases. - Zig isn’t fully baked yet.
Rust will be king of that space for a while, though.
I assume you're being facetious with the "recently came across", since scipy is so common, but I will add that nearly every math-intense library is a clever wrapper for some Fortran code.
I would be perfectly happy to see many hardened C libraries become the foundation of the next gen systems/ embedded languages. It does bother me slightly when we abandon the past entirely and attempt to "rewrite it in X"
But just like Fortran is still used for nearly every math-intense library, mostly invisible for most of the users, I suspect that C will still be around in 50 years from now.
I agree, with his observation that C should be no longer your language of choice for new projects. I personally still prefer using C/C++ for my private software projects, simply because it is the language I am most fluent in. This year I used it for AoC.
Not exclusively in C, not anymore: https://docs.kernel.org/rust/index.html
It might take another decade for a C-free build to be possible, though.
AFAIK Torvalds has also stipulated that any Rust code needs to be mirrored in C.
Do you have a cite for this? I hadn't heard this at all.
FWIW that's not the same thing? That's simply CONFIG_RUST=n, not "You have to write this driver twice in two different languages".
Rewriting that amount of code in ten years sounds very very hard, at least.
A potential strategy would be to set a date where any new drivers must be written in Rust. Deprecate every non-Rust driver on a date after that. Focus on rewriting only the drivers necessary for current hardware platforms (and some sensible/arbitrary cut-off going back x years). Then set a final deadline for a New Linux kernel release that removes all deprecated/C drivers.
I would blindly estimate that entire process to take 10-20 years (not including the time needed for debating the whole thing).
Then somebody "just" needs to rewrite the rest of the code.
So, uh, any volunteers?
Now, rewriting everything, including all the legacy drivers? Yeah, never happening even if Rust succeeds utterly.
I wish something like it could happen, but I am causyiously optimistic.
https://github.com/bjorn3/rustc_codegen_cranelift
The compiler frontend is already written in Rust.
Let's say it would avoid all the memory bugs in LLVM, that would doubtlessly make it worth throwing away the LLVM implementation (avoid sunk cost fallacy).
SciPy is a highly optimized library and Fortran is faster than C for some tasks:
Unfortunately it's an industry with a lot of stubborn old people so I don't see any major changes happening until they've retired but I'm willing to bet that async is going to revolutionize embedded development. Being able to await interrupts and doing efficient cooperative scheduling while writing straight forward code is a massive QoL improvement which is what embassy is enabling: https://embassy.dev/
Unfortunately Ada is held back by a genuine lack of awareness and some old misinformation baggage.
People normally think that C is the best for low level embedded programing, but from my experience, it pales in comparison to the amount of low level control and type safety that Ada provides, even when you restrict yourself to a small subset of the language (note: Ada is often used on barebone systems and without an Ada runtime). Given the interoperability Ada has with C, it means you don't have to rewrite everything in order to incorporate it into existing code bases.
It's a good thing more people are realizing we really need to stop settling for C and C++, stop investing more money into tools that simply compensate for the weak foundation of those languages, and finally adopt safer alternatives.
There are surelly other factors that weight in using C, but not for lack of options (in many cases).
Or maybe they will, given how much consultants in those languages happen to be paid, as no one else wants to touch them.
I do dream about that. Just being able to tag along while some programmers learn their Xth framework as a language.
Maybe someday I might concede and move on from C89 to C99.
I don't think there is any reason to choose C89 over C11 other than fear of new compiler bugs and compatibility with old compilers?
That's an extraordinary claim indeed; if people outside tech were going to call bullshit on EULAs as a liability shield, they would've done so in the last 50 years of software sales.
Reliability or the lack thereof has never been an impediment to some piece of software getting popular, but to me, you appear to believe that people want more reliability from software than they have been getting thus far.
Your belief is at odds with reality.
- Return of goods in digital stores
- Warranties in consulting projects, requiring free of charge fixes up to one year
- Cybersecurity bills
It will only get better from now onwards.
If anything, C may still be going strong for another 20 to 30 years.
One reason for C to disappear completely would be if computer architectures would change so much that current programming languages no longer even map to those new architectures (e.g. all the existing programming languages would need to be dumped anyway).
> The Lindy effect (also known as Lindy's Law[1]) is a theorized phenomenon by which the future life expectancy of some non-perishable things, like a technology or an idea, is proportional to their current age. Thus, the Lindy effect proposes the longer a period something has survived to exist or be used in the present, the longer its remaining life expectancy.
C does have a similar kind of niche at first glance: systems programming is of course conservative, rewriting everything is unlikely, and of course, C is the language used for ABI. Except on closer inspection, that moat is remarkably shallow. Being the language of ABI means that every competitor language has some way to speak C guaranteed, so the friction of rewriting systems software Ship-of-Theseus-style is much lower (though still nonzero). Systems software is rewritten from scratch on a much higher cadence: in the last 20 years or so, most of the userspace system glue for Linux has been replaced (e.g., systemd, iproute2, pulseaudio, wayland). And we've learned over the past few decades that there's no practical way to fix C's fundamental unsafety issues with software engineering practices, and C's committee is too conservative to consider retrofitting the necessary features to be able to fix unsafety at a language level (to say nothing of getting people to use it).
There is already a small clutch of languages that can serve C's niches that don't have the same fundamental unsafety issues, and right now, we're sort of at an experimental stage of system software trying them out. It's not unreasonable to believe that within a decade or so, one or more of these languages would be considered a standard, safe choice for implementing new systems software--and the use of C in new projects will start dropping. At some point, the proliferation of non-C systems projects will make people point out that having these components talk through C's ABI is too limited in functionality, and a system will change its ABI from C to some other language. And once it is no longer the language of ABI, C will lack its moat that keeps it alive, and it will start dying, though its death will be a slow, agonizing death.
Alongside RAII for resource management.
Namely, if it has to be "replaced", that would be with something with a much simpler syntax, which will require a bit more of finger power. We don't want to find ourself locked-in by very few compiler vendors (open source or not), that only because it is not reasonable to code a real-life alternative with a small team of averagely skilled devs in a reasonable amount of time.
This language should build on C though: no enum/typedef/_generic/switch/etc, only 1 loop keyword (loop{}) only explicit sized types, no integer promotion, no implicit casts (except maybe void* pointers but number literal casts should be) but explicit casts (compile-time and runtime, without that horrible c++ syntax), explicit compile-time const (we have only runtime consts which could be optimized as compile-time consts), enforce extern for functions (and don't try to put the binary format, elf/coff/etc, semantics into the language syntax or worse, the OS interface semantics), etc.
With enough discipline (and compiler warnings), we could get close to such language.
I did not check the latest and greatest rust syntax, but is what's above its explicit goals?
That said, I am a "everything in 64bits RISC-V assembly with x86_64/arm64 legacy ports kind of guy"... if RISC-V is successful (I wish). We could think of high-level language interpreters (coded in assembly, for instance a RISC-V coded python/lua/javascript/etc interpreters).
And something must be done about breaking changes of the syntax (or any never ending "features" additions).
We all know that some syntaxic constructs will be nasty in the end and will reasonably need fixing (basically, to keep them excrusiatingly simple from a compiler writing point of view). I am thinking about something like "syntax breakage provisions" from the language authors: for instance, no more than 4 iterations of major syntax breakages, then the language syntax will be frozen for forever.
Whatever, I am a everything in assembly kind of guy (with high-level language interpreters written themselves in assembly).
All that is a thought experiment for me, nothing more.
The "benchmark" to run to evaluate syntax complexity toxicity, for a compiled language, is how long an alternative compiler written by a small teams of averagely skilled devs, or even one individual averagely skilled dev, can stay a "real life" "working" alternative.
Mid-term/long-term planned obsolescence of computer language syntax is really...
Oh, and in my previous post: for compile time constants we can use static consts, my bad.