I understand why the OP likes C. I was looking at a couple of old projects of mine and I remember the satisfaction at least in one (a SASL library) of how I made various classes of error impossible using a fairly simple strategy and my own string handling routines. In a memory safe language with lots of assistance for threading there would never have been that pleasure. It was not amazing or anything but it was elegant to my eyes.
I feel I have to develop an economy of effort in C because there usually isn't a dumb and easy way to do things - this pleases me.
C has a mental model that seems relatively simple - I feel I can reason about what's going to happen to a greater degree than higher level languages.
C++ is the worst IMO because it has lots of abstractions but they don't make programming simpler because you pretty well have to understand the low level aspect of what they're doing to get performance - so the complexity is much greater.
Rust seems to put a lot of difficulty up front which is of no value in the kind of typical short program I write most of the time. I just didn't enjoy it. If I had something complicated to do I might eventually appreciate it.
The more high-level the language the less one tends to understand it and that's unsatisfying.
Having said all of that though, I tend to "just write python" whenever I can. If it's a throwaway program one doesn't need more and if it turns out to be necessary to interface to some library or other I know C well enough to sort that kind of thing out. I use threading.....never - at least never by choice.
I'm pretty much in the same boat and I don't even like Python the language. The ecosystem and extensibility is just so insanely productive it even rivals Excel...
In re Rust: I feel one of the mistakes most people make is taking Rust to be a general purpose "default language" for everything, like C and C++ used to be. But like you wouldn't write C++ for most stuff even if you were magically granted the ability to never ever write unsafe code, writing web services in Rust just means making your own life harder. It's a great tool, I'm happy it exists and I follow its development, but we must accept it exists in a different space than, say, Python or Ruby or Javascript.
I feel physical pain as I say this, but most stuff should probably just be Java.
> I feel physical pain as I say this, but most stuff should probably just be Java.
This has been on my mind a lot lately, causing similar anguish.
I'd note that, from what I hear, C# would also fit the bill, but be as unappealing to me.
I really wish that there was a language with the productivity, ecosystem and ergonomics of Python, with the speed and efficiency of Java and C# and a type system that increases productivity rather than word count.
Go seems... fine, but, I dunno, slightly regressive. I'll probably use it where performance and scaleability matter.
> I really wish that there was a language with the ~productivity~, ecosystem and ~ergonomics~ of Python, with the speed and efficiency of Java
That would be.. Java :D While I’m quite a big Java fan, for the other parts some other JVM language can easily fit the bill and you even get a choice. Scala, Kotlin, or maybe even Clojure if you don’t mind dynamic types.
Yeah maybe, I hear Java has improved markedly since my bad experiences with it (admittedly a long time ago now). I should swallow my distaste and give it a fair trial. From a distance it still looks somewhat verbose and unergonomic.
The others look like really really nice languages and I'd love to use Scala for a solo project.
However I guess I need something that's a) truly general purpose b) not going to scare away collaborators or potential hires c) going to be around for a long time d) not going to require evangilization at, say, a start-up.
Kotlin... I guess I'm wary of it being the new Coffescript - i.e. that it spurs Java into improving to such a degree that it removes the need for Kotlin in the first place.
And yes, having just spent a long time transitioning a project from JS to TS, I'm not sure I want to start anything serious without at least half-decent static typing - a major reason I'm looking for an alternative to Python, which is just not quite there.
For what it worths, Scala 3 is a really great language, it now even has a compiler option to exclude nulls, making you use something like `String | Null` to denote nullable values.
Yeah, from a purely personal point of view Scala 3, F# and, maybe, Elixir seem to offer the greatest promise of productivity, safety and expressiveness.
> Kotlin... I guess I'm wary of it being the new Coffescript - i.e. that it spurs Java into improving to such a degree that it removes the need for Kotlin in the first place.
I think that's unlikely. The key difference being that Coffeescript was always dependent on JavaScript because it compiled to JavaScript! Whereas Kotlin doesn't compile to Java, it compiles to JVM bytecode the same as Java does.
Java's gotten a lot better and maybe some day in the future I'll go back to general purpose java for server-side development, but there's enough velocity increase in Kotlin that I feel more and more productive. The real victory of the language IMHO is absolute null awareness (at least for largely Kotlin codebases). This makes any sort of nullability issue perfectly clear as long as you're not '!!' (null-safe opt out) everywhere like a lunatic (or as a newb when I started). Being able to seamlessly weave java and Kotlin in the same projects has made the slow iterative migration a lot simpler as well. All the same dev toolchains/ecosystems, just a different compiler plugin.
> I really wish that there was a language with the productivity, ecosystem and ergonomics of Python, with the speed and efficiency of Java and C# and a type system that increases productivity rather than word count.
>I really wish that there was a language with the productivity, ecosystem and ergonomics of Python, with the speed and efficiency of Java and C# and a type system that increases productivity rather than word count.
Nim has that except for the ecosystem. And it's also kind of janky, enough that I got so frustrated I stopped trying to use it. But you might have better luck than I did; check it out if you haven't.
Yeah I know lol, just trying to be honest. It's a genuinely fun language to work with; if it wasn't I wouldn't have put in so much time trying to make it work with the way I like to program. I still recommend you check it out, because people like to code in different ways, and it might fit your style of programming more than mine.
Iterators claim to be first-class but aren't. And "zero-cost" iterators that are only zero-cost if you don't value your own time spent debugging them. I've been able to crash the compiler when trying to write what in Python is idiomatic "itertools-y" code. And whenever I complain about this, someone pops up to recommend some third party iteration library. Which is part of the problem; I shouldn't need a third party library full of terrifying-looking macros just to write lazy iterators. They don't live up to what the docs promise, which is a real shame.
If they made iterators work how they seem they should work, I would come right back to Nim. It does so many things right but this one thing is a dealbreaker for me. The UFCS is practically begging me to chain lazy iterators together but actually trying it is like stepping on a rake.
What have you switched to from Nim? Have you gone back to Python and put up with the speed penalty or chosen something else?
Lazy iterators and generators are something I've missed in Rust. Haskell has them just by nature of everything being lazy and I love that. But I've become too familiar with Python, and unless runtime performance is a bottleneck, Haskell hasn't been the path of least resistance for most of my practical problems.
> What have you switched to from Nim? Have you gone back to Python and put up with the speed penalty or chosen something else?
Just Python and shell scripting. All this stuff is for hobbyist programming not my day job, so I just shelved the stuff that need performance and dusted off some projects that don't. I'm learning Rust at the minute but it's slower going, I'm not at the level where I can make the stuff I want.
I've been really enjoying Dart, I can write code slightly faster than in Python because of the type system and deep IDE support.
Unfortunately, when I use Dart, it's on Android, where the SAF exists, which kills my enjoyment, despite Android/Flutter otherwise being probably my favorite platform.
I really like Kotlin on the backend, it integrates nicely with Spring Boot, etc., but as soon as I say I have Kotlin experience, people say "Oh, so you're an android developer?"
I've never touched android development in my entire life.
Kotlin has its future assured thanks to godfather Google, stagnating Android Java on purpose (only recently they decided to finally move to Java 11 LTS subset), while promoting Koltin as its replacement for everything besides core platform.
As someone who equally loves C for all the reasons outlined here - I never understand the Java hate. Its an extremely practical, pragmatic language. Not every program has to be a technological marvel. Most code exists to get stuff done, and having code that gets stuff done and ISNT super complex is a pretty good thing, IMO.
I don't hate Java, I work with it every day and as you say it's a practical language that gets stuff done with minimal hassle.
The reason why it's hard to stomach is that it's the infrastructure of the future with the programming techniques of the past. Java the environment is like living in 2050, Java the language is like living in 1990.
Things are slowly getting better, but everything feels clunky and bureaucratic. The type system manages to be simultaneously ultra-verbose and not expressive enough to represent useful properties. It's a complete travesty that I need to codegen my own Tuples and write my own Either<T1, T2> as a literal Bohm-Berarducci construction. No value types (they're coming, but not yet stable). Didn't have Records until Java 18. Useless semantics for "final". Getters and setters. Nulls aren't even funny.
The language design feels at odds with the broader goal of providing a safe, efficient, solid programming environment.
It's still a fine choice for most applications, I'd venture to say it's actually the best choice, but that's because the language itself actually matters very little, unlike us nerds would like to think. But it doesn't have to be that way. I'm hopeful it'll get better.
I have found pleasure in combining „just write python“ with some C compiled code linked in and bridged via numpy for performance critical loop. I just love how fast I can write code in Python for when I do side projects
Pybind11 has significant overhead that makes it non-viable for some projects, even if it has a nice API. For pysimdjson we switched back to cython for an order of magnitude improvement.
Its magic function wrapping comes at a cost, trading ease of use for runtime performance. When you have a single C++ function to call that will run for a "long" time, pybind all the way. But pysimdjson tends to call a single function very quickly, and the overhead of a single function call is orders of magnitude slower than with cython when being explit with types and signatures. Wrap a class in pybind11 and cython and compare the stack trace between the two, and the difference is startling.
Ah yeah that makes sense. I would rather call a single C++ function from Python that calls other C++ functions (or itself). In case of pysimdjson however, Cython makes much more sense.
Overall this is way better than writing everything in Rust.
I've found myself thinking, time and again, is Rust exposing, to the programmer, what have traditionally been details handled to varying degrees by sophisticated compiler optimizations? If so, a great optimizing compiler might deliver binaries as performant as a Rust expert could, but with less programmer effort?
(I can't answer that, only haven't found a counterexample as a one time optimizing parallelizing compiler person.)
In my experience, the details Rust is exposing are less about performance and more about correctness. For example, Rust forces you to be explicit about:
There are some performance gains to be had in that list, but for the most part it's about asking the programmer to be explicit and avoiding convenient-but-risky defaults that lead to lots of bugs.
> a lot of difficulty up front which is of no value in the kind of typical short program I write most of the time
Wonder what are the examples of 'short programs' where you'd just zip out raw C as a default language, and it would be simpler/faster to implement than in Rust?
The comparison is really to python rather than C. I C mostly when I'm trying to fix some compilation bug in a library or when I'm trying to call some glibc or other OS level bit of code to achieve something- e.g. on a Raspberry Pico.
Using C for anything is just rare for me and C++ and Rust even more so because they don't add anything that I need usually.
I love C. I'm good at C. I have extensively researched undefined behavior, how to spot it, how to avoid it. I've written loads of C, and I consider my C to be pretty darn good.
I learned Rust over the last 6 months. I'm not sure I'll ever go back to C. I still love C. But Rust is the future. I'm pretty sure of it. I don't have to worry about undefined behavior unless I opt into it, and I usually write more performant and powerful code in a shorter amount of time.
I've gone down the road of trying to bolt on checkers and verification to C to achieve a better DX when writing safe C. C compilers just aren't extensible enough right now though (Clang would require some serious upgrades to the attributes and tablegen system in order for it to work well).
Rust's compiler is already extensible today, and the language has most of that built in.
Yes. I'll leave it at I have serious concerns about the safety of Zig and refuse to use the language or anything written with it until its creator changes his approach when responding to critical security vulnerability reports.
Andrew, you've taken this road for years now and have only been rude and dismissive to me on Discord, IRC and GitHub for a while, despite my many attempts to reach common ground and discuss what happened with the DOS vulnerability I found in the standard library, one you acknowledged was unfortunate. You dismissed it saying that Zig should not be used in production until v1, but I (correctly) pointed out that won't stop people from using it in production. Now, for example, we have Bun.sh, which worries me that the standard library has other "unfortunate" vulnerabilities you have also chosen to ignore that are making their way into production.
There's clearly nothing more I can say to you; I'm tired of the emotional and childish responses to my attempts to reach out. I've expressly avoided using your name and have tried to keep my critiques civil when discussing Zig the few times I have. However, you seem to find the comments every time despite this.
I wish you and Zig the best of luck.
---
andrewrk — 04/02/2020
there's no such thing as security vulnerabilities until post-1.0, which is why nobody should be using zig in production yet
I would love some context from either parent commenter here. This is the first I've heard of security concerns with zig, though admittedly I don't use it much.
Yes, I asked Dang to remove them after a conversation with Andy, as I wanted to reconcile this with him privately. Dang said he wouldn't remove the comments but would anonymize them, which I guess results in that username.
It's not really something I want to bring up again. I was asked my opinions on Zig, I gave them. The PR might not have been the 'best' solution but the vulnerability was left unaddressed - Andrew seems to insist I misunderstood something, but has failed several times to explain why.
I hope it's since been fixed, but panicking on UTF-8 decoding errors had the potential for massive damage in my opinion.
I've experimented and liked it, but the odds of a language exploding in popularity without a massive investment from tech giants is nearly zero. It's not "may the best language win."
Title and content dont really match do they? The article itself is basically this guys journey with C and the steps he takes to get something other languages now build in - ironically I think it was a stronger argument to write everything in Rust than any 'rust evangelism strike force' has put together.
Neat article, always interesting to take a peak at some ones highly idiosyncratic process.
Literally, yes. But I think most people come in expecting the author to justify the choice, otherwise this is no more interesting than "why I turned left at the intersection" or "why I have blue window shades".
If the author's reason is "because I can get the same features as Rust with much more effort" they've failed to justify it and thus that reading.
> so the article being an account of the guy's journey using C seems highly appropriate.
More importantly, it goes against the cargo cult narrative that the choice of programming language is the one and only factor determining memory safety, and consequently any use of languages such as C automatically leads to security issues.
It's not a coincidence that we have people in this discussion coping with that concept by spinning the use of frameworks focused on memory safety in C applications as somehow representing writing code in an entirely different programming language.
Even the author mentions that he intends to rewrite everything he ever writes in some memory safe language he's building.
Despite the authors borderline obsessive efforts to write memory safe C code, it's interesting to note that his implementation of "bc" prominently contains a file documenting memory issues in previous versions. If that's what borderline obsessive gets you, how are us normies who are only programming to pay the bills, who have to work with other people who also just want to finish what they are doing or interact with blobs of code written by other people with less exacting standards supposed to write memory safe code?
Yup, that's what I thought was the most interesting part of this, how many memory errors this guy has already admitted to releasing into production in this one project despite all his precautions.
If you wanna write your hobby programs in C because you think it's fun, then fine, you do you. But this seems like a pretty good indication that basically no C written by anyone can realistically be truly safe.
That was my favorite part - it was incredibly virtuous behavior to lay it all out like that. The article would not have been interesting if it had omitted those details.
The point in Rust and C# and Java and others is that memory safety is either mandatory or opt-out instead of opt-in. This means you have some guarantees about the whole ecosystem.
Of course you can augment a language with libraries. But you are one developer and everyone else will make different choices.
> This means you have some guarantees about the whole ecosystem.
Sure, you are free to make this sort of claims. But they are besides the point, aren't they?
The cliche being discussed here is C's hypothetical ties with memory safety issues, and here we are commenting on a discussion on how someone writes C using a framework focused on memory safety and thus providing "some guarantees" about their whole ecosystem. Yet, we're still seeing comments mindlessly parotting myths in spite of the facts that are being dangled right in front of their nose.
I feel like you are missing the point. Using glibc in avionics software is insane, as is any other application software library. In other words, it’s asinine to pretend C is somehow more reliable because it’s used in safety critical applications. There simply isn’t anything in common with the C running on your servers and the C running on an avionics package.
I don't run servers much, I mainly write for critical applications (a decade writing for geophysical airframes, telling pilots where to go and where the pylons are when 80m off the deck for millions of line kilometres, lot's of other stuff going back a long way).
> Using glibc in avionics software is insane,
As I said above .. there's no need for glibc et al.
> I feel like you are missing the point.
We seem to be in agreement here.
> In other words, it’s asinine to pretend C is somehow more reliable because ...
What most people call "C" is (preprocessor) + (actual language) + (stdc function library).
What I call C (and hey, maybe that's just me) is just the language component, the "below the fold" library spec in the ANSI Std is easily disregarded .. it's more of a dated "proof of concept" of (for example) one of many ways of handling strings.
C is a nice language - the preprocessor and stdlib have issues.
> As I said above .. there's no need for glibc et al.
Christ, I wrote it as an absurd notion in the first place. This is honestly like writing “go ahead and wear underwear on your head” only to have someone tell repeated tell you that isn’t necessary. Well, no shit.
Really? That read to me a a self aware, even humble recognition of personal shortcomings. But I see communication as inherently more difficult than keeping an idea in one's own head.
C is fun - there's elegance in writing good C code and the mental model you need to understand it is much simpler than the higher level languages.
It's also fun to write data-structures yourself instead of being a mere user all the time. In C you can tell yourself that your take on how to do datastructures is worthwhile and unique (might be a slight stretch) but in other languages almost anything you can do is redundant.
I used to write loads of C, and recently I came back to write some C bindings to a Rust library, and it was _not_ fun. It was dead simple from the C side - mostly just a few functions that constructed or handled opaque pointers. The problem is the error handling is atrocious, and that was so hard to present neatly. The consequence is an overly verbose API in which I've essentially manually monomorphised in C every result. For sure, it's not idiomatic C, but it is properly handling errors, which arguably is one of the main failures of idiomatic C.
It's not just the API either. So once you've got your "nice" error handling API, you have to manually handle resource clean up - no branches that exit for you; every function call is accompanied by a result check and a goto to cleanup code (so your pointers need to be defined properly prior to any calls and initialised to null). Honestly, I don't know I ever lived with this mess. It was a painful and laborious experience to get to something that was just about ok.
> So once you've got your "nice" error handling API, you have to manually handle resource clean up - no branches that exit for you; every function call is accompanied by a result check and a goto to cleanup code (so your pointers need to be defined properly prior to any calls and initialised to null).
I have a stack allocator that will release everything allocated in the function on exit, and I have macros to replace my use of the return keyword that also deallocate everything in the stack allocator before returning.
So my code actually has code cleanup on return.
C does not have to be laborious. Maybe the fact that I have done things like that is part of why I like C?
Where can I can get such a stack cleanup system? Also, can you call arbitrary clean up code (this is hardware that needs cleaning up as well as structures allocated in rust that need to freed in rust)?
I use a stack allocator that knows about setjmp()/longjmp(), functions, scopes, and destructors. (Destructors are a function pointer type in my code.)
Basically, you tell the stack allocator to store a jmp_buf, then you setjmp() on it. Then keep going.
For a function, a special destructor is used as a marker. Same with scopes.
The same is also true of a setjmp() destructor, which is actually what activates longjmp().
The stack allocator should have three functions to unwind itself until it reaches the end of the next scope, function, or jmp, respectively.
Then, when you need an exception, or to exit a function or scope, you call the correct stack allocator function to clean up everything to that point. Then you return or whatever.
This can run arbitrary code on cleanup for an item if you write a "destructor" with that arbitrary code. For example, one idea I had was to use the stack allocator to ensure a mutex or other lock was always released. This would be done by storing a pointer to the mutex and writing a "destructor" that unlocks the mutex when given a pointer to the mutex. Boom. Scope-based mutex unlocking.
I think people use the word "simple" to mean different things. You mean "easy to do," while other people mean "easy to understand what's happening." C lovers love the second definition of simple.
“C lovers” love the appearance of understanding what happens. At the end of the day they still feed their programs to awe inspiring optimizing compilers. Sure, they can probably explain what any snippet of code in their program does, but they can’t tell what, if anything, from that snippet is executed by the machine.
You only need to know, by heart, _a_ list (even a subset) of the documented behaviour of C, you can simply refer to the standard whenever you happen upon something not in your list of known good (or known undefined) behaviour. It's entirely possible to, assuming you can keep this up honestly, write perfectly well defined C without hitting undefined behaviour. It is equivalent to writing brainfuck in a language which is a strict superset of brainfuck. As long as you pick a turing complete subset of C to know the semantics of off by heart, you can write perfectly safe C (the only obstacle being human fallibility). The idea that you need to learn all the documented undefined behaviours in C is a myth and is, in fact, fundamentally wrong on the basis that the documented undefined behaviours in the C standard are only an infinitesimal subset of the set of undefined behaviours in C given that anything not explicitly defined by the C standard is automatically undefined.
If you have a mine-field with an uncountable number of mines, knowing the locations of 200 mines won't help you cross the mine-field safely. If, instead, you learn how to spot areas which are known to be safe, you can, assuming you don't make a mistake, at least attempt to cross it safely (and if you fail to cross it, you can go back and learn how to spot other areas which are known to be safe).
That is the difference between your claim that you need to know all the documented instances of UB and my claim that you just need to know enough defined behaviour to write your program.
It has nothing to do with security assessments or pentests.
Yes, like enjoying wood work == stockholm syndrome, why not go to Ikea??
Also to gp, title perfectly matches, pointing out why he believes in memory safety when using C, everytime also pointing out the flaws with his thinking, and why this only applies to him in his completely owned projects. Not sure what's wrong with the critics here.
Great write-up on the author's personal perspective. This part in particular I think is a good summary of why this works for him:
The next reason is that during my time with C, I have developed a custom software stack designed around memory safety.
[...]
In other words, I am not using C; I am actually using the partially memory-safe Gavin D. Howard dialect of C.
In other words, don't try this at home? I definitely should not try it at home.
> I can write C that frees memory properly...that basically doesn't suffer from memory corruption...I can do that, because I'm controlling heaven and earth in my software. It makes it very hard to compose software. Because even if you and I both know how to write memory safe C, it's very hard for us to have an interface boundary where we can agree about who does what.
What a clear way to describe the difficulty with manual memory management.
So like Lisp, C might end up being a solo programmer’s tool too. But not because it is a breeding ground for DSLs but because everyone is adhering to different conventions in the absence of the compiler enforcing one.
I don't think that the composability/interface-boundary/memory-mgmt argument stands for C and much even less so for C++. There's nothing difficult about it nor ambiguous.
Did you actually watch the linked presentation? If so, I would be curious in an earnest rebuttal: I very much stand by my conclusions -- and even more so after having spent the years since on Rust-based systems and C/Rust hybrids. Specifically, I have not seen a BTree implementation in C that I would consider to be composable; can you point me to one?
I did not watch the presentation, sorry. Depends what the definition of composability you have in mind so if you share what you mean by "composable BTree" implementation, I would be happy to give my opinion. I ask because there's no much I can see about the composability of a b-tree implementation other than supporting multiple types, and this is certainly doable although not as elegant as in C++.
While I appreciate your candor, you may want to know what argument, exactly, you are putatively refuting? (Specifically: the argument is not about types -- it's about ownership.) If it's not too much to ask, take two minutes to watch the linked clip. As for the composable BTree implementation, I actually am not asking for your opinion -- I'm asking for a link to the implementation itself. The details here matter (and indeed, that's the whole point).
I watched the video now and it is literally what one of the parent comments paraphrased and what I was responding to.
What you said is:
> I can write C that frees memory properly...that basically doesn't suffer from memory corruption...I can do that, because I'm controlling heaven and earth in my software. It makes it very hard to compose software. Because even if you and I both know how to write memory safe C, it's very hard for us to have an interface boundary where we can agree about who does what.
And I stand by my point. Single-responsibility principle will get you covered, yes, even in C.
> I actually am not asking for your opinion --
You could show some respect.
> I'm asking for a link to the implementation itself. The details here matter
B-tree btw is a silly example which particularly is not among the hard design problems out there and there are dozens of implementations laying around. Obvious examples to look for would be in transactional database systems implemented in C.
> (and indeed, that's the whole point).
... and the point not being quite clear, even after politely asking for a clarification, so perhaps next time you don't try to play the authority but try to provide an actual example to support your otherwise questionable claim. I'm sure Rust community will profit from individuals like yourself.
I saw it expressed by multiple people whose language of choice was Lisp but the only one that I specifically remember is Ron Garret’s recent post called Lisping at JPL Revisited[1]:
All this is a reflection of the so-called Lisp curse, the fundamental problem with Lisp -- its great strength is simultaneously its great weakness. It is super-simple to customize Lisp to suit your personal tastes, and so everyone does, and so you end up with a fragmented ecosystem of little sub-languages, not all of which (to put it mildly) are particularly well designed.
He doesn’t specifically say here though that this makes Lisp a solo tool. I can’t find a source for that right now unfortunately.
Lisp is the most powerful and elegant language. That is a problem because you're giving weak, fallible monkeys phenomenal cosmic powers. Now, many monkeys are well-behaved and disciplined, so they create reasonable, pleasant codebases. But some monkeys go bananas.
I also like to program in C; the fundamental error is not programming in C (or liking C), but thinking that you’re special or uniquely able to use C safely.
The author adequately defends their predilection for C, which was never doubted; they don’t produce a great argument for why C is good (which, perhaps, they didn’t intend to). Most of the defensive programming techniques covered in the post reflect this: asserts, for example, only catch the bugs you know about.
Exactly. Objectively, there's no such thing as a disciplined and effective C programmer. Everybody makes mistakes. You can't guarantee that you won't. Believing that you can is in itself dangerous. It's delusional. Experienced programmers know they make mistakes and even anticipate that they will. My attitude these days is "I wonder where I messed up; let's find out!". Basically, like a good scientist, I try to falsify the hypothesis that I messed things up. Only when I start failing to do so, I move on. I make all sorts of silly mistakes. Off by 1 errors, logic errors, inverted conditions, etc. You name it, I do it.
There are all sorts of valid arguments for not immediately porting over the huge legacy of C/C++ code bases to something less likely to blow up in your face. However, the amount of valid arguments for starting new code bases in those languages is rapidly declining. I'm sure there are some corners where you just have no other valid choice and that's fine. But increasingly it's just a matter of people being stubborn, rigid, and unwilling. Half the success is just understanding your own limitations and mitigating those. The rest is just dealing with the inevitability of people messing up.
I have yet to touch Rust, as I am currently busy with other things and my work is in other computer language. And mostly glueing modules together.
But yes, I am one of those weirdos that use C for their personal projects, and yes I find it "fun" to fire the debugger to look for my own mistake, see my code run magnitude slower through Valgrind to find my memory errors, and yes I know that my code will be riddled with UB and other kind of "fun" error, but again that is expected because (well) it is C. And I do it on my commute while ssh-ing with my phone.
And while I am very impressed by the progress of Rust over the years. There are two aspects that kind of put me off.
The community does come as very preachy and the language sound very close to the Ada language ( as in "not fun to work with"). And so far those two haven't been disproven.
So maybe the day that I would have to write life critical software as a hobby I would consider writing in it in Rust. Or maybe in order for Rust to succeed it would have to make the other languages far less fun.
Rust is actually really fun to work with. Don't let random naysayers on hn convince you otherwise, try it for a month or three and form your own opinion.
There is something magical about wri5ing code that is as fast as C++ but with a fraction of the bugs and effort.
About the fun aspect of using Rust. Once I learned it, nothing feels as fun as Rust for two reasons - when I'm done writing the program it almost always works first try and second, when it runs I know this is the fastest possible program that I could have written.
It takes a while to get to the point where writing Rust feels easy, but when you do it's so much fun. That's why Rust users love the language so much.
To elaborate on the 2nd part: idiomatic Rust is usually slower than the absolute fastest program you could have written. Making sure you are not allocating (usually wrt to Strings and Vecs) in the hot path often takes a non trivial amount of work compared to naive solution (for ex, fighting with borrow checker instead of cloning, or or allocating your Vecs and passing results thru a mut ref parameter vs a return value) and also makes your code uglier.
That said, writing an ultra performance program in Rust is easier than using any other language in existence rn.
>This means that I get many of the advantages of Rust
Arguably, you don't. The big difference is whether your compiler enforces and tracks your documentation, assumptions used by your code, often critically important for correctness. Unfortunately, Rust is far from being perfect in this regard (e.g. SPARK probably does a better job here), but it significantly improves on C/C++ in this regard.
>I Work Alone
Unfortunately, even if you are the only soul touching the code, it often quickly becomes false. On a big enough project, "alone" inevitably becomes "me and that guy who wrote this part of the code". This also can be formulated as "me and 2-years-ago-me". In other words, we quickly forget a lot of context used for writing a piece of code.
>Rust is too big for my small brain, unfortunately.
I think the lack of hubris, the understanding that small human brains are not enough to keep track of all necessary context required for writing correct software is the key element for liking Rust and similar languages. Rust is not about building ivory towers of abstractions for the sake of it (though some people try to walk in this direction), but about offloading as much as possible of the context on compiler. Unlike humans, the compiler does not get tired, it does not forget. Yes, it's limited and because of those limitations it requires to restructure our code and sprinkle it with additional information, but more often than note it's a good thing.
As sel4 and other formally verified software demonstrates, it absolutely can be done in C. But one could say that C used in sel4 is not a C language anymore. After all, you can not take a random C library and use it in sel4.
> When it comes to the enjoyment of programming, I hate all of them. With a passion.
> Except for C.
I get the sense that this is the real reason, and the rest of it is justification that they understand the risks involved but choose to do it anyways. And honestly, I think this it's fine? I'd argue that it's a little inconsistent with their statement above that avoiding C/C++ in lieu of memory safe languages and should be followed by default, but they subsequently admit to being hypocritical about this, so I'm not really concerned that they don't understand the benefits of memory safety. When it comes down to it, I have middling confidence that I would be able convince someone who still at this point doubts that Rust is safer than C, but I have absolutely no illusions that I can convince someone to change a personal preference. If anything, it's a bit refreshing to see it freely admitted, given how often it seems to be the real reason for a lot of choices of what language to use (and I don't just mean this for C/C++, but for any language, including Rust).
I think he's fairly transparent that it is the only reason. "I program in C because I enjoy it even though I know objectively it is not a terribly good choice for building software" is pretty much the entire article.
There are 2 things I've obsessed about in C & C++ land:
1) Why are we explicitly directing a loop forward or backward most of the time when it's not necessary. The compiler should direct the loop unless we want to explicitly do this for the body of the loop. My thinking is that there may be performance reasons to reverse a loop or even split it into separate threads. (unlikely)
2) In C++, I've always wondered how template types get instantiated for POD that is represented the same at the bit level. Does the compiler duplicate the templated code or is it smart enough to say "this std::vector<int> is the same as that std::vector<intalias>". Rust prides itself on zero-cost abstractions, I have wondered if they reduce binary bloat in this area. (as a naive C++ person)
1) The compiler is already allowed to reorder loop evaluation if it can prove that it can't have any observable side effects. If it can't, there could be subtle dependencies on the order of evaluation that the person writing the code and tests wasn't aware of, so you're just asking for subtle bugs that are missed by tests and could even turn into vulnerabilities in production.
2) It duplicates, and the standard even comes with asinine "guarantees" that actively sabotage deduplication, such as requiring all functions to have different addresses. Which means the compiler can reuse the same function, but only one function label can start right at the code and the others need to NOP-slide or JMP into it. Which unduly pessimizes precisely those functions that are most likely candidates for deduplication: Those that themselves only consist of 2-3 ALU instructions.
Nr (2) is not correct. Compilers have been inlining functions for decades so the argument of "all functions to have different addresses" does not hold. Compiler can even _merge_ two different but similarly looking functions into a single one. And that's just part of the code transformations compilers can do.
And this goes without even mentioning LTO, which has been explicitly designed for optimizing the code layout and size during the linking phase.
So, code deduplication is a real thing and happens regularly with your code. Templates are no much different than the non-templated code written for the same purpose.
If you take the address of different functions, the standard requires those to compare unequal.
However if you merely call functions and never take their address, the standard doesn't put any requirements on that, so inlining etc. is legal.
But without LTO, it's impossible for the compiler to know whether a non-inline function with external linkage will be called or have its address taken, so the compiler must ensure that it has a unique address.
There are linker level optimizations aside outside of LTO that can merge functions. gold's --icf option comes to mind, with --icf=safe meant to be conforming.
All that sounds ... reasonable? What I didn't agree with is the "requiring all functions to have different addresses" argument. It's invalidated both by the compiler (e.g. inlining) and linker (e.g. code folding).
If it can prove that nothing can observe the address of the function(s). Inlining renders the whole discussion moot.
The point stands. Compilers cannot merge equivalent functions in cases when it makes sense to even think about this optimization, which is when it actually has to export an externally visible symbol for the function.
And that's what happens in probably like 99% of the cases. I objdump the code quite often to understand what happens under the hood and rarely I see that the similar code has been duplicated. There could be of course examples but I just didn't agree with "standard ... actively sabotage deduplication" sentiment which to me reads as something universal and not an exception.
I think active sabotage is a correct assessment when a simple, obvious optimization is explicitly prohibited for no (defensible) reason and can only be applied by extensive whole-program analysis that allows the nonsensical rule to be bypassed completely. It's still sabotage, it's just mitigated by the extremely smart compilers we have nowadays that basically pick the program apart and rewrite a functionally equivalent one.
In regards to #1, I mean we shouldn't be asked to explicitly direct the iteration of the loop. We should say "loop over these things" rather than "start from 1 and stop at 10". MOST of the time we're explicitly stating these things - that the compiler can undo - so what's the point? If the compiler is doing whatever it likes, why are we expending thought on which direction a loop should iterate?
I love that Rust has eliminated the egoism around "you should know at all times when something is allocated or deallocated". We're trusting the compiler to do that better with zero cost to us. So take it to the extreme: Only specify the loop direction if you have a true requirement to do so.
This is my Ted Talk, thank you.
For #2 - LOL! I had no idea it was essentially aliasing those functions to get around a requirement for uniqueness.
1 isn't really the same thing. I don't know any compiler that would turn
for (i = 0; i < length; i++)
into
for (i = length; i != 0; i--)
Which is what I read 1 to be.
As for why you sometimes do one over the other. I think it is true that computers branch on zero more easily than other values. That said, I'd be a little surprised to know it makes a difference that can be measured in any meaningful program.
See this is why I felt I was obsessing over something very small.
Loop inversion, unrolling, and splitting. The compiler can do so much with a loop to make it run faster on 'this machine' to optimize for speed, memory use, or binary size.
I couldn't quickly figure out what sort of applications this person writes for a living. But for 95% of applications I can think of: whatever, you do you.
Lots of people have pointed out flaws in his arguments here. There are always at least two collaborators on a codebase: you and you in two years time. You can enforce your own documentation best practice, but it's always possible to make mistakes unless you have a probably correct way of enforcing those best practices. I'm sure there are other very valid criticisms.
But in the end, there's a good chance that 100% guaranteed memory safety actually doesn't matter for the software that write. If you told me this person writes software for pacemakers or ABS systems or something similarly safety critical I'd take issue. But beyond that it's a sliding scale from merely irresponsible to an inconvenience.
Reading the section on how the author just likes C much more than other languages, I was going to comment he should try building his own C-inspired language that he feels comfortable with but fixes some of C's flaws. But it turns out that's what he's already doing: https://gavinhoward.com/2018/08/pl-design-1-principles/
I like C better than every other language... for some of the same reasons. Mind, I'd prefer assembly but it's inconsistent between different versions of the same processor, let alone different processors ...
I CAN code in a lot of languages. The only one I have no interest in ever revisiting is PHP. I rather dislike java's bytecode but the language itself is somewhat ok.
But I prefer C. Not just due to "bare metal", but also because it's fairly easy to communicate with other hardware.
I'll note - when multiple different hardware is involved, you'll never get "safe code". Not unless the hardware is "safe" too. I think the thing I'm wary about rust about is how much its claims depend on the underlying hardware being consistent, reliable and stable.
But - I'll use whatever tools employers or contractors need. I think the hardest language for me is C++ - as above - because it's so "flexible" (and hides so much under the water) that it takes rather a lot of investment to learn every new stack.
I use C on my own projects. I've yet to see a good argument to explore another option and still maintain consistency with communicating with disparate hardware.
I stayed away from C++ for about 15 years, while working in higher level languages (C#, Go). I recently came back to it and my productivity is SOOO much higher, along with my code quality.
I argue that for certain applications, the ability to work directly with memory is paramount. I shaved 35ns off of a critical routine the other day with a clever memory hack. And in another example, I doubled the speed of a critical loop simply by rearranging variables within a struct so that the CPU had more efficient access to them by avoiding page cache faults.
Mind you, this code already runs hundreds of times faster than anything written in Go or any other garbage collection language can do.
> Rust is too big for my small brain, unfortunately.
Rust is harder because it imposes constraints on the programmer in order to enforce memory safety. These constraints don't magically disappear if you are trying to write memory safe code in C.
The difference is that you can just use any kind of valid constrains or invariants to prove safety in C (but unfortunately these are just in mind or documentation and not verified by compiler), while you have to fit to specific model of constraints used by Rust.
The constraints imposed by Rust are more difficult to deal with though. There are so many cases where I know for sure 100% that something is completely safe, but I have to spend a whole lot of brain power trying to come up with a way to convince Rust that it's completely safe which doesn't just involve unnecessarily boxing a value.
We've all been there – you know for sure something is 100% safe and then the fuzzer finds and UB that was outside of your mental model bounds...
Thing is, if some system/concept/abstraction/model is memory-safe, more often than not it directly translates to Rust concepts via lifetimes and the such. If you've done it a number of times, you will find it very easy to map the former to the latter. In the rare cases when it doesn't fully translate, you can always dip into unsafe (usually at the lowest levels of your abstractions), same as the Rust's standard library does, and test/fuzz that part extensively.
I am not against the concept of safety guarantees.
We have all been there where we have written code based on a faulty mental model which resulted in bugs. Yes. And more often than not, Rust would have prevented those bugs or made them less severe. But we have also all been there where we have had a solid mental model of what a system is doing and written correct code.
Rust helps in the former case, but it makes the latter case harder.
And no, it's not the case that "more often than not", if you need one const reference and one non-const reference to some data, your code is wrong. In fact, almost all code in almost all mainstream languages end up with multiple references where at least some are non-const, and it's fine most of the time. In Rust, all of those situations require type system and borrow checker gymnastics.
I'm not against Rust. I think it's a fine language. I also think it's significantly harder to write than most other languages if you don't want to just punt and use Rc<RefCell<T>> all over the place.
Rust can't encode non linear lifetime at compile time. This means anything where ownership or access patterns can be self referential or "go up" statically. This includes things like intrusive data structures (linked lists, graphs) and concurrent/callback based control flow. Rust doesn't have the constraints to model these so they do in fact "disappear" (at least statically) as far as both languages are concerned.
The good thing about C is that, while its memory management is unsafe, it is at least explicit; there's generally no doubt as to whether a statement allocates memory or not, since "malloc" and "free" have to be written out explicitly in the code.
The whole bulleted list from "Custom Memory Safety"... this makes me want to run away towards the horizon and scream.
In the 90's it was very common that big shops that did some programming would have their own languages, which continued into the 00's in the form of everyone rolling their own C++ standard library. I had the "pleasure" to work with both. Invariably, this stuff was so much worse than off-the-shelf stuff and yet the authors believed themselves to be geniuses.
I mean, there's a chance this guy did something outstanding and actually improved something about C, but so far I've seen so many fail to do that in such a predictable patterned way I just don't want to see any more of that.
My favorite C code is the one that has no macros, as few of typedefs as possible, no acrobatics with alloca() or any platform-dependent stuff. I think, FIO is a good example of what I'm talking about.
The whole thing just sounds like "yeah, it's a swamp, but it's my swamp and I like my swamp!" kind of thing.
> (...) big shops that did some programming would have their own languages, which continued into the 00's in the form of everyone rolling their own C++ standard library.
It sounds like you're talking about using libraries as if it was a bad thing.
> had the "pleasure" to work with both. Invariably, this stuff was so much worse than off-the-shelf stuff (...)
Where do you think "off-the-shelf" stuff comes from? It sounds like you're trying to mistepresent the adoption of subjectively sub-optimal libraries as somehow the issue caused by using libraries and frameworks.
> mean, there's a chance this guy did something outstanding and actually improved something about C, but so far I've seen so many fail to do that in such a predictable patterned way I just don't want to see any more of that.
You're showing a tremendously naive and misguided belief that libraries are somehow bad.
Languages such as Rust flourish with the adoption of third party libraries,some of them euphemistically described as "not stable". Languages such as C++ flourish with third-party components like Boost and POCO. But applying the same principle to a language like C warrants an automatic and mindless putdown like blindly accusing anything of being bad? It makes no sense.
A more generous interpretation would be that if you have a communal library, rather than everyone having their own individual libraries, it's easier to vet it for correctness. Rust takes the communal approach. The parent was suggesting that every company has an insular library approach. NIH.
Many people that go off the beaten track often fall into the same ditch.
Correctness yes but what about other requirements people have such as performance, extended functionality, domain-specific optimizations, predictability, etc.
Third-party libraries were not born out of the thin air but because of different parties having different and very much often disjoint requirements so it's not particularly NIH syndrome.
There is a huge gap between something like boost that is designed to be reused and internal libraries that are subject to the usual corporate constraints (the least amount of work is done if even that).
> There is a huge gap between something like boost that is designed to be reused and internal libraries that are subject to the usual corporate constraints (the least amount of work is done if even that)
You're somehow trying to imply that libraries are not reusable if they are not widely shared, which makes no sense at all.
The best argument you could make is regarding stability, but you simply cannot lay any such claim just from the library's licensing.
Accusing internal libraries of being half-baked, badly designed, or bug-riddled is just a cheap blanket putdown.
> It sounds like you're talking about using libraries as if it was a bad thing.
I don't see the relationship... how do you make such a leap?
> Where do you think "off-the-shelf" stuff comes from?
I worked on proprietary projects that eventually went public domain. So, I can tell you where off-the-shelf comes from from personal experience. Proprietary in-house tools have the benefit of not having to deal with many things that an off-the-shelf tool would have to deal because they can choose the kind of hardware to run on, the kind of supporting libraries, their versions and combinations to use, the kind of developer tools to support and so on. off-the-shelf tools, in order to be successful must cover a much wider area of ecosystem in order to be relevant.
Here to give you a better example: C doesn't have its own concurrency primitives, no concurrency model etc. But, you could build an extension, let's call it CC to have some sort of concurrency based on pthreads library. If, for your internal needs, you only use CC on Linux -- you are golden and everything works fine. Once you try to make CC into off-the-shelf product you have to do something about pthreads missing from other platforms.
Off-the-shelf products are typically born as in-house products, but they need an extra step to become what their name implies.
In the specific case of what OP described as their own extension to C, I can vividly imagine a language that doesn't cut it as an off-the-shelf one. Making an off-the-shelf language would've also guided OP to a much larger departure from C because memory safety is only one of the big problems with the language. And this is what happened to a bunch of other languages which already walked a big chunk of that departure journey. This is why I would've been frustrated having to use the language like OP's: it would've felt like not enough change and too much headache to adjust to my target environment than could be had by using actual off-the-shelf languages.
> You're showing a tremendously naive and misguided belief that libraries are somehow bad.
I have no idea where you get this from. If anything, I say the exact opposite...
C++ is the F-35. Much touted. Expensive to maintain. Expensive to get rid of. Doesn't quite live up to the hype. Borland C++ and Visual C++: never the two shall meet. They might as well been entirely different languages.
Given all software problems that plagued F-35 during all its development, including mid-flight avionics reboots, I don't think Bjarne Stroustroup actually realizes that it isn't a good idea to advertise it.
> My favorite C code is the one that has no macros, as few of typedefs as possible, no acrobatics with alloca() or any platform-dependent stuff.
You've just outlined a perfectly good subset of C. I'm pretty sure people who write C extensively even to this date mostly end up there. The beauty of using C these days is that one gets to control every single byte without going through hoops and loops. Also, any C code can be reliably integrated into other languages using FFI, which is quite universal. So, yeah, no need to create dirty hacks.
> The beauty of using C these days is that one gets to control every single byte
No, not even close: Struct padding, compiling switch as if-else vs. jump, implicit arithmetic promotion to int, stack layout of local variables, and many other issues.
If you want to control every byte, write in assembly language.
Nit: struct padding and arithmetic promotion don't support this point. In practice, C compilers provide struct-packing extensions that cover the uncommon cases where default padding wouldn't yield the layout you want. And they'll let you opt-in to a style where arithmetic promotions are forced to be explicit as they are in e.g. Go. So it would usually be ridiculous to drop down to assembly for either of these reasons today.
Well put. Another way to think about it: every major OS and systems vendor has had some custom “safe” C variant over the years, to empirically mediocre results. It's unlikely that this author has done better; it’s much more likely they simply haven’t received the same scrutiny.
Software developers tend to love things that give them challenges and material for mental workouts, and it's fine.
However, it has nothing to do with commercial software development, where system architects should think not only about the "coolness" of technology, but also about people who will support the final product. And team leaders think about how hard it would be to hire people who know how (and are willing!) to work with the said "cool technology", and since we are talking about statements like "I have developed a custom software stack designed around memory safety", i.e. non-standard solution with no documentation, (while there are alternatives, where memory safety assumed to be as a pre-requisite, not as an implementation artifact), actually, "close to zero".
I was writing in C and classic C++ for 17 years, and I admit it is fun and challenging to design and implement systems using these tools. However, when in my team we have a library that we're expected to develop and write in C, I rewrite it in modern C++17 (I don't say "in Rust" exactly because of the second reason, but modern C++ is safer than old C by an order of magnitude), and the process rarely takes not more than 1 week. As a result its source code usually becomes about 10 times smaller, implementation faster in 80%, and the team is spared of wasting their time exploring undocumented solutions written decades ago.
> The Rule Of 2 is: Pick no more than 2 of untrustworthy inputs; unsafe implementation language; and high privilege.
I do know of a few C programmers I'd trust: DJB, many of the OpenBSD team, the Dovecot maintainers, and a few others with long track records of security.
But I don't trust myself because I've used fuzzers on my Rust code, trying billions of inputs. And I've found DDOS bugs that would have been potential exploits in C.
What's more damning, the most careful C code I ever wrote has an enormous, sneaky test suite. It was tested with every sanitizer I could find. It used carefully designed error handling conventions. Still, in the last 20 years, it has been the subject of several CVEs. You see, I relied on a high-quality 3rd party XML parser, and that parser had a handful of bugs.
Out of 7 billion people on this planet, the number that I'd personally trust to reliaby write CVE-free C code is in the low triple digits. I'm not one of them.
Understanding Rust is a cakewalk compared to understanding "undefined behavior" in the C standard, or to making sure a large C program never overflows an addition, or accesses memory out of bounds. But Rust is not the only option. Use Go or Java or Ada SPARK or a functional language or WUFFS. Any are fine.
As an industry, we need to stop making the same endless security mistakes. It's not OK.
> You see, I relied on a high-quality 3rd party XML parser, and that parser had a handful of bugs.
At that point it's hardly the fault of C. Even in supposedly "memory safe" languages like Java, XML (and json) parsers are often riddled with bugs and design problems that can be exploited for remote execution. It's a joke, but these parsers are still somehow considered "best in class" because many supposed "programmers" are willing to trade security for the ability to write annotations.
I'd say that what you describe in your software is exactly what i like about C. You know it's dangerous, so you take precautions. You then discover that those precautions maybe aren't enough and you go looking for something more stringent (like rust). There's learning there. If we cargo cult Rust as "what people should learn instead of C because it's secure" bad programmers will go write poorly designed rust code that's as unsafe (if not more so) as any C code, except now they won't be careful because "the language is safe".
>I'd say that what you describe in your software is exactly what i like about C. You know it's dangerous, so you take precautions. You then discover that those precautions maybe aren't enough and you go looking for something more stringent (like rust). There's learning there. If we cargo cult Rust as "what people should learn instead of C because it's secure" bad programmers will go write poorly designed rust code that's as unsafe (if not more so) as any C code, except now they won't be careful because "the language is safe".
What level of cope is this? "Bad language is better because we know its bad so we become better!" Every time someone argues for C or C++ I replace them with assembly and C respectively. The argument makes about the same amount of sense.
I don’t think that’s what they’re saying. They’re saying that there is utility in understanding what you’re getting with a memory-safe language like Rust. Why would a programmer care if they’re getting memory safety and thread safety if they don’t know what unsafeness looks like? It’s the same reason we teach history in school; knowing where we came from gives us a better perspective of where we are today.
They can read the Rustonomicon. Done. They can also write C or Unsafe Rust in order to learn about what regular Rust gives them.
But none of that is an argument for learning C and then stumbling into a safer language after doing that, since then you have to learn (say) Rust as well as unlearn your C (or whatever unsafe language habits) as well.[1]
>It’s the same reason we teach history in school; knowing where we came from gives us a better perspective of where we are today.
What we learn from history is mostly "holy shit that was fucking awful thank god we stopped doing that!". Except for programmers it seems. I have no objection to learning or using C but we don't ride horse caravans around anymore.
> Even in supposedly "memory safe" languages like Java, XML (and json) parsers are often riddled with bugs and design problems that can be exploited for remote execution
The huge benefit of having a strict compiler like Rust's is that it massively raises the quality floor for every single library in the ecosystem. This is especially true if a library doesn't use unsafe (which is true of many Rust libraries including the most popular XML library https://lib.rs/crates/quick-xml).
> bad programmers will go write poorly designed rust code that's as unsafe (if not more so) as any C code, except now they won't be careful because "the language is safe".
The whole point of the language being safe is that you can't write code as bad as C code (e.g. contained RCE vulnerabilities) without going massively out of your way by using an `unsafe` block or doing something obviously stupid like passing an unsanitised string to `exec`. It won't compile. I would absolutely trust carelessly coded safe Rust code over carefully coded C (unless that carefulness is taken to the extreme as in MISRA-C or similar).
> Even in supposedly "memory safe" languages like Java, XML (and json) parsers are often riddled with bugs and design problems that can be exploited for remote execution.
Why do we think this is OK, even for a moment? For how many decades will we accept massive vulnerabilities everywhere? We can't rewrite everything, but why isn't Chrome's "Rule of Two" a minimum professional standard for new code?
This isn't just a Rust thing. For example, why don't more languages allow me to say, "This XML parser is forbidden from talking to the network or file system"? Why could log4j load code without being granted special permission?
Because that is a lot easier said than done, all told? As we get faster and faster machines with more and more spare cycles, we can probably devote a lot to this sort of thing. Maybe.
And, really, this is no different than anything else. Just in an odd "reverse" direction. Lets say someone makes brake pads for your car that have an antenna and compute on them. How would you keep them from communicating your location to anyone else? You can work on detection, of course, but for the VAST majority of vehicles out there, this would be silly at that point.
Same goes for why you use locks that can typically be unlocked with a toothpick in bathrooms. Privacy locks are vastly different from security locks. Even though they look roughly the same on paper. And this is clearly ignoring every other thing that makes you vulnerable outside. Why do we allow satellite imagery of people in their backyard? Mainly because we don't have a realistic way of disallowing it.
I think there is an opportunity here for language design. I should be able to import a library and mark it as nothing but computation, or as network only no file system, or file system only but no network, and have the language itself enforce that guarantee. I believe it has to be at the language/compilation level as executable segments can always just trap into the OS.
If there were some syntax, then you could even specify the particular network endpoints or file system paths allowed.
It is sort of old fashioned, a sort of dynamic scoping of access context, but would be better than you call a shared library which starts up some threads with network connections all invisible to you.
There would be some idea of the main function and spawning threads or green threads would also need to specify what that thread can do, a subset of what the creating thread can do.
> I should be able to import a library and mark it as nothing but computation, or as network only no file system, or file system only but no network, and have the language itself enforce that guarantee.
There is no sheriff to enforce this guarantee. There's nobody come to save you (us) from ourselves. You can try to build walls around things, but meanwhile other people are building ladders (or digging tunnels).
It gets worse though. Virtuality, whether via literal VMs or COWbuilder-style containers, means that defining what is means to access certain resources is hard to do. For the most part, this resolves nicely (i.e. you almost always have less significant access to "the real world" than you might believe), but it's not trivial to determine.
It's funny to read those threads of people saying how nice would it be to have, or that things are impossible, and yet those same things are there in Haskell since forever.
So tell me, I run a Haskell program inside a VM and it accesses the root of the filesystem. What happens? It accesses the network interface, what happens?
The point is that the code, at runtime, cannot determine what context it is running in, and it may be entirely appropriate for it to do certain things, or highly inappropriate, depending on that context.
Yes, this, exactly. In our current operating systems models, the process is the most granular resource that we can confine. The OS model doesn't allow for more fine-grained resource control than that, so code with different authorization scopes should run in different processes.
Yeah, the replies all sort of show ways of wedging this in, but why not add it to the language/compiler? It would be very useful, and also interesting to see attacks against it. It would also be interesting to see how this sort of dynamic scoped feature would be implemented these days.
(In old LISP stuff, you used to be able to do like:
That sort of redirection will absolutely be possible, but that won't be the language-level thing my language will support.
In my language, there are packages. You might have a left-pad package, for example. (Okay, it's a bad example, but whatever.) The programmer/user gives permissions to packages at the package level, so you might give the left-pad no permissions at all except for allocating memory (for generating a formatted string), which means something else will need to do the output.
These permissions can include anything, like being able to use other packages, network access, filesystem access, etc. Static dependencies will be the first line of defense, but if something need dynamic access, say access to specific files such as `/dev/urandom` or `$HOME/.config/<program>/config.txt`, there will be a dynamic way to give permission.
This dynamic way will probably serve a lot of the use case you suggest.
This will be at the compiler IR level, by the way. The build system of dependencies might otherwise be able to get around it. But with IR, you can tell the dependency's build system to generate the LLVM-like IR for its code, and its build is done and can have no more influence.
Then without using any of the dependency's code, you can then specialize the IR while inserting permission checks.
You can even use this to generate cross-platform "binaries". The "binaries" would just be a tarball of IR. Most IR would be used for any platform, but some would be platform-specific. If you include all of the platform-specific IR with the platform-independent stuff, you can then ship that tarball to any machine, which, on trying to run it for the first time, will realize it needs to be specialized for that machine. This includes inserting permission checks based on the policy for that machine.
Yes, it will be that powerful. My main concern is that it will be too powerful and thus have bad user experience or a steep learning curve. UX matters.
It is a culture thing. The damage that broken code has caused was either not big enough to shift that culture, or (more likely) everybody involved convinced themselves that software errors are both inevitable and a welcome excuse to pin things onto — just try how often some privacy leak/hack/error is blamed on "software failure" as if this was one of gods lightning bolts nobody can protect against.
If we did electrical engineering like we did software every gadget would come with a fire extinguisher. If we did civil engineering like we did software engineering we would have daily bridge collapses.
In the dawn of modern civil and electrical engineering indeed many catastrophes happened, and they did happen till culture changed and had an inpact on the education of future engineers. This point has not been reached at all with software engineering outside of safety critical applications.
For example, why don't more languages allow me to say, "This XML parser is forbidden from talking to the network or file system"?
Because that's not up to the programming language to decide. In which context a particular piece of code is executed is out of scope for the design and compilation phase of your program -- that's part of the application deployment, and that deployment is usually not written in the same language as your program.
What you're asking for is basically the microkernel approach applied to applications: each microservice has its own capabilities and data access, and control is passed through RPC. This exists, and it can be implemented in any language (though more readily in some than in others), but this is not a programming language feature: it's a runtime feature that must be provided by the environment (whether JVM, Linux, Docker, SeL4 or browser) in which your code is deployed.
> Because that's not up to the programming language to decide.
There's actually a lot of really neat research in this space, including:
- "Row types" for effect systems, which allow the compiler to cleanly keep track of what code has access to what parts of the outside world.
- Capability systems, where you essentially need "handles" to access things in the outside world.
- Strong-typed unikernals, which use either of the above approaches to replace the process boundaries in a regular microkernel.
Admittedly, none of this is exactly ready for the mainstream yet. But there are plenty of ways to help address these issues at the programming language level. And I think this is worthwhile, because as the Chrome team has pointed out several times, trying to stick every parser in a seccomp-isolated process is a lot of hard work. And it rarely gets done in practice.
Given the sheer number of dependencies many projects have the days, and the growing number of "supply chain" attacks against libraries, I think this is worth all the effort that's going into right now.
Maybe in 10 or 20 years, we'll see some commercially-acceptable languages that explore this space.
Even people like DJB have written buffer overrun vulns in their C code. In my opinion, it is impossible for an organization of meaningful size to write and maintain a program of meaningful complexity over a period of time in C or C++ (rather than some very strongly constrained version of C++ or a constrained and augmented version of C) without introducing memory safety errors.
I mostly agree, but I also feel it hasn't really been attempted properly, by which I mean exploiting the type checker to its fullest. For instance, write a stdlib where all functions reading input return "unsanitized" data types that must be supplied with a validation check before the sanitized value can be read out. Safer strings and arrays, possibly references instead of pointers, return explicit error values rather than errno, etc. And then require a program to only write against the safelib.
I suspect most C programmers just wouldn't like the constraints, or would be concerned with performance degradation, and that's the real problem.
Edit: another avenue is to also use Frama-C to check your code.
You can do this. You can ban all pointer arithmetic, use array types that have attached sizes and automatically introduce bounds checks, require various compiler extensions and annotations for lifetimes, require initialization immediately upon declaration, have the compiler introduce nullptr checks when it cannot prove a pointer is nonnull, ban std::variant and reinterpret_cast<T>, and more.
You end up with something that resembles the set of programming requirements from Rust (if you leverage lifetime annotations) or you end up with something resembling a refcounting GC (if you demand the use of shared_ptr everywhere).
This cannot really be "attempted properly" at scale. To implement this with C you need to both superset and subset the language. To implement this with C++ you need to harshly subset the language. Both communities hold backwards compatibility as a huge goal, making it impossible to move in this direction. A project like Carbon seeks to incrementally move in this direction, but required breaking off from the C++ community. Projects like Rust just rip the band-aid off entirely at the beginning.
The "don't worry, C is fine if you just don't suck at programming" folks don't tend to push for these extreme changes.
I'm not even thinking about compiler extensions, just standard opaque pointers and fat pointers, and macros/functions that operate on them, and a linter that flags any references to unsafe C stdlib functions or uninitialized locals. This won't be as safe as Rust, but you'll at least still be in C. I just think we can be a lot further along the safety spectrum in C than we currently are, it's just C programmers still use some outdated practices and idioms that could be safer.
The point is that you can get some safety guarantees. Nowhere near the degree of guarantees available in safer languages, but still better than the status quo.
Edit: one example of a pattern, whose name I can't remember, was to switch from returning pointers to data structures, to returning pointer offsets as handles. Using these handles you can then track more information about validity and handle "null pointers" more sensibly rather than it introducing undefined behaviour. In superscalar processors the offset calculation basically costs nothing, but the additional safety can be considerable. I believe I read about this pattern in game engines, let me know if you know the name of it!
You can get some safety guarantees. Many of these things are very good choices. I own a very large C++ codebase that uses this approach you mention all over the place.
But you can still see important limitations. Chrome, for example, has custom smart pointers (miracle_ptr) and still has UAFs galore.
"Out of 7 billion people on this planet, the number that I'd personally trust to reliaby write CVE-free C code is in the low triple digits. I'm not one of them."
My number is zero. I do not believe it is possible to build a truly safe C program of any meaningful complexity.
> Specifically, I do not think I'm smart enough to violate the Chrome team's Rule of 2
I simply don't write only C, or these days much C at all. What I write mostly is Python, and if I can't optimize that enough then I write C; or call C. Of course when you're calling somebody else's libs, all bets are off; and I don't think it matters what language the lib is written in.
The last time I needed a serde that "went to eleven" I wrote it in Rust using PyO3. I'm not saying it was good Rust. I wouldn't know. The compiler is a bitch. It's too hard to reason about the machine. Luckily, I wasn't asking it to do much except some byte hacking in a buffer.
Reasoning about the machine is becoming more problematic as exotic architectures proliferate and compilers unceasingly do increasingly weirder optimizing shit. Dunno if it matters what the language is.
Overlow panics in debug mode. And in release mode, overflow is defined to wrap (2's complement) instead of invoking undefined behavior. One of the big dangers in C is "nasal demons", after all: https://en.wikipedia.org/wiki/Undefined_behavior
But I've fuzzed a gnarly parser written in Rust, and actually found overflows. None of those overflows were exploitable (as more than a DDOS), because safe Rust also does bounds-checking on arrays and slices.
Rust isn't perfect here. But in practice, it manages to mitigate almost everything anyways, because of boring ancient techniques like bounds checking. And the excellent fuzzing tooling makes it super easy to test parsers aggressively.
My favorite analogy for “only debug builds check this” is wearing a floatation device at the dock but throwing it overboard to sail into a storm.
Quiet overflow was sort of defensible on 4 MHz machines, but now Costco has desktops with a dozen cores each a thousand times faster. Moore’s law has more than paid for bounds checking and overflow exceptions (or even bignums) and we should be using them by default.
It checks in debug mode, but these checks are turned off in release mode by default.
They can be turned on in release mode, but C/C++ could add `-ftrapv` too[0]...
You also have access to `checked_mul` and friends, so manually writing `i.checked_mul(j)` will add checks[1], without needing to use undocumented vendor specific builtins, like in C/C++[2].
It has controllable behavior, explicit APIs to pick the precise behavior and there is no undefined behavior with regards to overflows which greatly helps with memory safety.
It is controllable in real C compilers too (i.e. -fwrapv, -ftrapv GCC options), so you can make them silently ignored or cause trap. Real problem with integer overflows is that they are logic errors that are hard to check in compile-time, and neither C nor Rust helps with preventing them.
I’m not sure that’s true but I also don’t have data on this. In my personal experience the issue with integer over-/underflows mostly played a role when those integers were used in branching or memory indexing. The rules around UB with overflows are really messy and those issues definitely do not exist in Rust.
the problem in C is that so much behavior is undefined which implies unreachable for the compiler. this means that C style UB can time travel, delete checks, delete whole functions and trash the world. rust (and most other languages) just define overflow to wrap which is way safer because you aren't letting the compiler mess with your code using assumptions that might be false.
If you need C like memory access (embedded, performance..) I highly suggest zig, which is maturing really fast.
We use rust and zig in house for our development and while rust is preferred for complex projects, we prefer zig when we need: super portable code, code that will work with a static memory buffer or datastructre that are hard to use in rust (like self referencing).
We still have some chips that requires C because they have proprietary compiler, for those, we are very eager for the zig->C compiler that should be coming some day.
I'd argue that compile-time is really nice exactly because it can be mixed with runtime. It takes some time to get a good grip of it, but in the end I realized that a lot of code can actually be made comptime.
I agree that async is peculiar, but I think it is still too early to decide if this works or not.
Compile-time evaluation is easier if you think of it as a separate language for pre-computing and generating zig code where most of the time it's used to supply the type of a parameter or construct a container like you would with a template.
You have copy-paste operators like `inline for`, `inline while`, and `switch (x) { inline else => {} }` that focus on generating code given a value known at compile-time (think of it like running a C program to print out the code within a `for` loop then inlining that in your code) and are great when you need to unroll a loop or loop over the fields of a struct (e.g when printing it). If you find those difficult then try printing the code they inline to get a feel for how it changes your program or writing what they'd inline manually.
For the more template style `ArrayList(T)` style you can follow the logic yourself by writing the struct it would result in when you substitute the parameters (again a copy paste but with slightly more going on) and any more calls with the same parameters will always return the same struct back.
Branching at compile-time is like having a smarter `#ifdef` for the most part where you use `if`, `switch`, and other control-flow to decide which code should remain in the program as long as the condition is known at compile-time. This logic can be applied to the copy-paste operators too depending on the condition.
Async is very experimental and not complete so it's currently difficult for most. It can be thought of as a fancy way of creating a state machine with `.nextState()` being the `resume` keyword making it not a function but by having the compiler perform a little magic at compile-time it's possible to write code which doesn't care about this until you get to function pointers as then the difference starts to matter. If you want a C equivalent have a look at https://c9x.me/articles/gthreads/intro.html which covers it pretty well and it's almost what zig does for you with the `suspend` and `resume` keywords. `async` and `await` should be understood in terms of `suspend` and `resume` as they exist at a much higher level and can be a bit tricky. Another way to get a quick overview of what it's doing is to have a look at generators in python with `yield` and their equivalent with writing the state machine yourself as a class as it's equivalent.
If you feel like trying it again, don't be afraid to reach out on the zig room on matrix. I'll be happy to help with comptime and related things as it can be a bit difficult to grasp.
Oh and don't give up! The best parts about learning is the rewarding feeling when it finally clicks after all the hard work.
Why was the "why" removed from the title? The author is writing about why they're doing something, not announcing to the world that they're doing the thing.
This comes across as rationalization and investment bias. Because someone "likes" something doesn't make it better, it makes it philosophy or religion.
You can put guards on a razor blade, but it's still going to slice your finger.
The title should be "Why I use C despite believing in memory safety (but you should use something safer that you prefer)". That better expresses the author's point and avoids the pointless comments about how hard it is to write C safely.
Basically, the author says that he feels unproductive in other languages so he's better off doing a good job in C than not finishing in other languages, and he enumerates techniques to reduce memory errors, freely admitting that other languages give that by default.
Yikes. I do not think you have thought through the implications of licensing (or purporting to license) under the union of three contradictory licenses.
I didn't read anything past that, because this is very much a "only way to win is not to play" kind of situation.
I'm currently making a language that translates directly to C.
There won't be pointers, and I think everything will be passed by value.
I'm not sure that compilers can optimize this and "move" returned data. But the goal is to greatly limit side effects and improve cache locality by encouraging the coder to use more functional habits instead of oop.
There are grey areas because I'm not sure the design is sound, but even if it's not, it can be safe and fast enough.
It is absolutely possible to develop robust, complex applications that you can trust using C. I think the best example of this is SQLite.
But, we need to recognize the size and coverage of the test suite involved with this product... I would be curious if any maintainers of SQLite have some commentary regarding the impact of unsafe programming on the extent of the testing.
Then again, SQLite is notable exactly because of how exceptional it is. Indeed, most C projects can’t afford to be developed and tested in such a way. So the problem isn’t that C can’t be made safe, practically any language can be made safe, instead it’s that C can’t be made safe within the resource constraints of typical development.
I also like writing code in C both professionally and for personal projects.
One thing I LOVE about C is the offline documentation for standard libraries. Beej’s guide to network programming is amazing but also reproducible with “man socket” and then wandering down the rabbit hole. (Ref: https://beej.us/guide/bgnet/html/split/)
C++, the sprawling beast that it is, you have no choice but to consult some web page to get decent documentation. The same goes for Rust (Please let me know if I am wrong here, I have not used Rust in several years).
Maybe it’s just a personal quirk, but when programming I love to shut off Wi-Fi and focus on whatever it is I want to get done. It is not always feasible of course, but it is definitely my preferred way to work. And C is the most amenable language to being offline.
Controversial opinion, somewhat philosophical: I use C because I still believe in freedom, and I think you should too.
If we gradually destroy all semblance of individual thought and discretion and replace that with something that essentially approaches rule-by-machine, what remains is not far from an authoritarian dystopia in societal terms. Even more so when you realise the direction Big Tech is taking. Jailbreaking and rooting are an escape hatch that they're certainly trying to close with all the "memory safety" stuff.
C also has an advantage of being simple enough that a single person can write a compiler for a nearly full subset of it. The same can't be said of many other languages.
I'm not saying I can write 100% correct code, although I certainly try (and IMHO it's not too difficult to get close.)
"Freedom is not worth having if it does not include the freedom to make mistakes."
I think equating a compiler that stops you from making runtime mistakes to authoritarian social dystopia is ungrounded, baseless and frankly dumb. My idea of freedom is not wasting my life debugging null ref errors on weekends because somebody else wants the freedom to do bad ideas.
to me the assertion has merit from the point of view that being able to understand what your application does completely is much easier in a small language like C or lisp/scheme, which i believe is essential to maintaining the proper master -> tool relationship with the computer. it seems straigh forward to me that if you use things you dont understand it is likely you will open your application to all sorts of potential unexpected behaviour
Having worked on C and C++ compilers for a few years at this point: I would say that there’s a strong negative correlation between claiming that C is a simple language and actually understanding C, as specified.
C is actually a fantastically complicated language, both syntactically and at the semantic and abstract machine level.
while i said its small, not simple, i do not think it is controversial to say that c is simplER than rust if what you aim for is a complete understanding of your aplication
I think that is controversial: Rust has more complex syntax than C does, but has a substantially simpler semantic model. Just a small example: given just `T*`, neither you nor your compiler knows what your global aliasing set is. The compiler has to do significantly more work to prove exclusive ownership (necessary for many optimizations), and you have no formal guarantee that your data doesn’t change underneath you.
They’re comparable, as evidenced by both being representable within existing compiler IRs. But no, I said that C’s semantics are significantly more complex.
my view of c is as an abstraction over assembly. from this perspective i just cannot see that rust provides simpler semantics, unless you are trying to make c do something which you should probably use another language for, like lisp :)
This is what I mean: C is absolutely not a naive abstraction over assembly. The C standard is clear on this: C is defined in terms of an abstract machine, which in turn doesn’t cleanly map to any particular machine architecture.
Treating C like a “high level assembler” needs a rule like the cryptographic doom principle: it will inevitably lead you astray, and “astray” for C generally means memory unsafety.
> Treating C like a “high level assembler” needs a rule like the cryptographic doom principle: it will inevitably lead you astray, and “astray” for C generally means memory unsafety
you are right. however, i think c does a better job at being a model of an abstract machine than rust. if we restrict semantics to that, and the developer accepts that they need more knowledge than just c, i think the semantics are simpler than rust's. in the same way, dont you think that rust can create a false sense of security to those not completely understanding what protections it provides? if we see programming languages as a hci problem, then i think c by default offers more freedom, and consequently more unsafety. amyway i dont want to come off as hating rust, i think it is a good language, but more importantly its community seems fantastic and i wish it success
> dont you think that rust can create a false sense of security to those not completely understanding what protections it provides?
No: the entire point of Rust's design is that you don't need to know the abstract machine semantics to write Rust safely. The compiler enforces the semantics for you, rather than pushing them onto you.
C on its own does feel like a simpler language though I think it’s true that the semantics taken literally undermine it (the generated assembly can be really surprising sometimes). I do trust a given Rust program to have a specific runtime behavior much more when optimized. It might be interesting to come up with a variant of C that has genuinely simpler semantics as well (not sure how useful I’d find it though)
It’s a common argument that a more simple language makes for more understandable code, yet practical programming tells a different story. From web browsers to every major C compiler, C++ is the language of choice. Similarly for GUI frameworks, GPGPU programming, etc. So what is it? Are we supposed to believe the, frankly pompous, notion that those developers/firms/institutions/etc were tricked? No, I think not.
If we are being honest with ourselves the truth is that even a language as nightmarishly complex as C++ is preferable to C. And whether or not you agree with that the fact is that the world marches on regardless.
Rust is a defined space. It doesn't restrict your freedom of thought or speech on your personal GitHub, you can use C or brainfuck there just fine.
Rust just says "This is an engineering project, not a philosophy paper, or science lab. It is not the time or place for experimentation, new ideas, and individual discretion, or for trusting each other or ourselves, we want reliability at all costs".
Which is exactly the attitude I want to see, when failure means I lose important notes, money, or get lost because GPS crashed, or my battery runs down because everyone was too afraid to optimize their brittle bug heaps, or a self driving car murders me, or we return to 1970s level tech due to nobody trusting software.
You can absolutely code in Rust for fun or experimentation. There's absolutely room for new ideas and individual expression in Rust. It's not a straight jacket. It's like you and the compiler are pair programming together; it's a collaborator, not a task master.
> Rust just says "This is an engineering project, not a philosophy paper, or science lab. It is not the time or place for experimentation, new ideas, and individual discretion, or for trusting each other or ourselves, we want reliability at all costs".
I'm rather fascinated that you put this in these terms, because I've seen the exact opposite (specifically the philosophy paper). The few times I've seen rust tried out, it seemed to turn into a competition between devs about how clever they could be with category theory. Then an abstraction stack grows until logic errors creep in because nobody understands what the thing is actually doing anymore.
I don't want to blame rust specifically for this, because the same thing happened with scala. Not sure what it is, possibly just a lack of dev discipline. I can't help but view this category of languages (rust, scala, haskell, etc) as competitive philosophy because I have to spend several hours sitting around pondering the nature of things before I can make minor changes from my jira ticket.
End result ends up being that someone usually re-writes an "interim" solution in java/go/etc, over a couple of sprints and then nobody ever gets back to the long term solution written in the elegant language because they don't have time to understand it.
As a counterpoint, I wrote an "interim solution" in Rust in a few weeks that ended up replacing both the python and C++ versions that had been months in the making. I suspect the difference might be more about the skill and pragmatism of the developers involved than the languages used. Heavily functional code is not consisdered idiomatic in Rust.
They're all plenty skilled, but I'll give you the pragmatism one. A certain style of dev seems to have a compulsion to violate Kernighan's Law.
Eventually that abstraction hierarchy collapses on itself. I don't want to pin this purely on rust either, since I've seen it in plenty of languages (including java), but I see it happen often enough that I'm reluctant to trust my co-workers with rust at this point.
Or maybe they all can and I'll retreat into my little abstraction-minimal kingdom where the code's not very elegant, but I can figure it out, and I can onboard people in less than a month.
I feel like this is trying to solve an ethics issue using technology. It would seem more appropriate to fight for freedom by joining or supporting the people who already fight for it, than sitting and thinking by using C one helps the world in that regard.
I’ve seen you post a variant of this comment on multiple Rust-adjacent threads, and I still don’t understand it: Rust was by no means the first “safe” language, it wasn’t the one pushed by Big Tech (that’s Go, another good language!), and there’s no particular evidence that preventing jailbreaks is the actual, secret intention here. That last part borders on conspiracy, given that engineers are overwhelmingly anti-DRM and pro-modification.
given that engineers are overwhelmingly anti-DRM and pro-modification.
If they were, things wouldn't be the way they are today. Perhaps you're saying that they are, but can only express that position in the form of subtle exploits (which seem to be decreasing over time)? No, I think the majority of engineers are overwhelmingly complacent and obedient, and won't oppose.
As for why Rust, that's because the amount of prosletysing of that language seems to be far more than all the others.
Dont mind the downvotes, I somewhat agree with you with the the lack of freedom argument (although not quite sure about the larger context around dystopia).
To put it in other words, I want my tools to do whatever I want them to do and not the other way around.
I want my tools to do whatever I meant to write. If I made what I consider a mistake, I want a tool to point it out. And I want memory safety, because without that, terrible things can happen that nobody ever wrote.
pointing mistakes is the job of the linter, the job of the compiler is to run my program asap and even try to fix my mistakes automatically. There is nothing more pedantic than a compiler saying "Oh you shouldnt do this as it can potentially be a race condition"
Is there no semblance of individual thought in C++? No discretion in Python? Are you ruled-by-machine if you use Go? Is using Javascript an authoritarian dystopia?
Apparently the road to hell is paved with vague slogans...
I don't agree with the grandparent post but can offer an interpretation of what they mean. Languages like Go, Rust, Java, etc. are enormous and are designed and managed by a committee. Sometimes, they go in a direction that you might not like. For example, many people were pissed off by the compatibility break between Python 2 and 3. If you find yourself disagreeing with the designers' decisions, you have very little recourse but to stick with an old version of the language/tooling while the rest of the world moves forward, because you are in no position to understand and maintain a huge language implementation codebase. The grandparent is implying that C is so simple that if you disagree with the direction of the official C language and major compiler vendors, you can simply write fork the language and reasonably your own compiler. But of course, I would say that you have the freedom to reinvent the same bugs over and over again, that the committee was trying to proactively solve for you.
> I run Clang and GCC with -Wall, -pedantic, and -Werror. This means I eliminate all warnings at every stage of development.
> I even run Clang with -Weverything
This isn't "discipline". Those warnings themselves aren't all tested and often don't work. There's no point in conforming to a broken check like -Wstrict-aliasing or one that could be arbitrarily hard to fix like -Wdeprecated.
I have a similar issue. I like the memory safety in Rust, but I don't like Rust: it limits my productivity for two reasons. 1. In order to reach memory safety it add abstractions that don't make programming simpler, but harder. 2. It is a language that started to enter the slippery slope of too many ad-hoc features. And given that I work alone, I try to write safe C code as much as I can (it is impossible to reach perfection, anyway), and avoid Rust as much as I can. What I will do more, however, is using more other safe languages when the C low-level control is not needed. Go is a good option, for my tastes.
P.S. I see in realtime, reloading the post, the number of downvotes received (the score goes up and then ways down with the downvotes). Another problem is the Rust community. It has a very peculiar way to stay in this world.
> I don't like Rust: it limits my productivity [...] I try to write safe C code
It's definitely fair to argue that Rust is complex. But this doesn't compute.
You don't like Rust because you're less productive with it so you write... C? Rust is so much more productive compared to C it's not even funny, even with its limitations and extra complexity that are necessary for memory safety. Things that take pages of code in C can be done in a single line of code in Rust.
This starts at fairly fundamental data structures. Need a hash map? In Rust you have one available, out-of-box, with state-of-art performance. In C? Implement one yourself. And good luck getting even a fraction of the performance the Rust one has.
Then there is dependency management and a huge ecosystem of libraries. Do you need to, say, encode an AVIF image? (Something I personally had to do.) Easy. `cargo add ravif`, and two lines of code later you're done.
I can keep on going, but I'm sure you get the idea.
I really honestly don't see how anyone can in good faith argue about productivity and say that C (out of all things!) is more productive than Rust. That Rust is more complex, sure. That you don't like it for aesthetic reasons? Fair enough. But not as productive as C? No way. Perhaps this is where the downvotes you're complaining about come from?
I bet when you say "Rust is more productive than C", what you actually mean is "the Rust stdlib and its wider library ecosystem enabled by Cargo is more productive than what the C stdlib offers", and that is no doubt true. But when you look at just the language, the OPs statement is also no doubt true.
The libraries that make C more productive are out there, they are just harder to find than in the Rust ecosystem.
IME C programmers (including myself) separate a language and its stdlib much more strictly than (for instance) the Rust, C++ or Python crowd, mainly because the C stdlib is pretty much useless for anything that's not a simple command line tool (and because of such restrictions, C programmers also often use a whole language toolbox instead of attempting to write everything in C - Python and C go very well together for instance, right tool for the job etc...).
> I bet when you say "Rust is more productive than C", what you actually mean is "the Rust stdlib and its wider library ecosystem enabled by Cargo is more productive than what the C stdlib offers", and that is no doubt true. But when you look at just the language, the OPs statement is also no doubt true.
No. I also mean that the language itself is more productive, even without the standard library and the tooling. (Although, indeed, to a lesser degree.)
Sum types. Pattern matching. No need to bother with header files. A module system. Proper generics. AST-based hygienic better macros. Procedural macros. Stronger type system. RAII. A proper string type which knows its length. A proper array type which knows its length. Unicode support*. Async + await support. Support for closures. Ergonomic error handling (with the '?' operator). Proper abstraction for iterators.
Do you want me to keep going? Because I can keep going. (:
Again, this is more complex than what you have in C. And it is harder to learn. But that's not what we're talking about. We're talking about pure productivity here, so I'm assuming on one side you have someone who has mastered C, and on the other side you have someone who has mastered Rust. There's just no contest that the Rust person will get things done significantly faster than the C person, because the language itself is so much more powerful. (But of course it's a lot harder to master Rust, so the calculus might be different if you don't want to master your tools.)
* - part of Rust's core so also available in `no_std` contexts, so I don't see it as part of the stdlib but part of the language.
Some language features from your list I agree with (e.g. Rust's "everything is an expression" is really nice), others are just batshit-crazy from my pov (the macro system for instance), others are just more or less random tack-on features (e.g. I don't really see the usefulness of async-await in a systems programming language - especially not when it potentially fragments the library ecosystem).
I would still argue that none of those language features really move the productivity needle a lot into one or the other direction. A good library ecosystem on the other hand does make a difference.
All in all, I would probably add fewer features to C than I would remove from Rust to get close to what I expect from a performance-oriented systems programming language (Zig is actually pretty close to that ideal, but that's a different discussion).
> others are just batshit-crazy from my pov (the macro system for instance)
Ah yes, I won't disagree here; macros get very complex very fast. It's one of the more undercooked parts of Rust.
> e.g. I don't really see the usefulness of async-await in a systems programming language
This really depends on the task, but honestly it is genuinely useful. Even in places you wouldn't expect, e.g. in embedded: https://github.com/embassy-rs/embassy
And AFAIK there are plans to use async Rust in the Linux kernel.
I've heard this often, but I'm not convinced. C just lacks mechanisms to accommodate something like generic data structures, all the libraries I've found feel very hacky, it's either void pointer or macro overload.
The limits you are referring are not proper of C, but mostly of its standard library. Any long term C programmer has libraries, written by herself or by others, that will limit this problem a lot. An example, starting from your own example. I need a dictionary? I take my rax library, written for Redis. I need dynamic strings? I use SDS (and limit a lot the memory safety issues I could have). C is not well understood because it is seen as the language + its library. But the undefined behaviours and the terrible library are not C original sins, but the terrible work of the ANSI C committee. The Ansi C committee is responsible for a lot of bad things in software in recent years.
But the problem is that C fundamentally limits reusability! I have great respect for you and redis, but your examples are telling: they deal only with strings. With Rust or C++ you can have an extremely efficient, well tested, robust generic map structure that will accept any key or value types. C simply cannot do that, because of the language's limitations, not just its stdlib.
But C can be improved by adding features that allow this and there is a lot of CS experience accumulated around metaprogramming to design this without lots of pitfalls, and no one says that this is easy, this is really hard, but possible. Unfortunately, the committee plays with old and ugly macros system and a little bit with lambda which for sure is better than nothing, but not enough to make C better.
> This starts at fairly fundamental data structures. Need a hash map? In Rust you have one available, out-of-box, with state-of-art performance. In C? Implement one yourself. And good luck getting even a fraction of the performance the Rust one has.
I've implemented a hash map in my code. I loved the experience, and I know more about computers and hash maps now that I've done it.
And my hash map was faster than the one in the C++ STL last I checked. I haven't benchmarked it against the Rust one, though.
> And my hash map was faster than the one in the C++ STL last I checked.
Last time I checked, benchmarking hash-maps, or FWIW any other data structure, in the vacuum did not make much sense to me. Microbenchmarks cannot mimic or predict end user workloads well enough, at least in the cases where it's worth it.
That said, each implementation has its own trade-offs and in particular the one from C++ STL opted in for the pointer stability. For that reason alone, which I think is quite worthy, open-addressing conflict resolution is out of the question because it cannot give such guarantees.
With that in mind, if you're ok that a pointer to your key-value pair can all of the sudden be _invalidated_ by the mere operation of inserting or erasing yet another element from your hash-map, then of course, you can trade robustness for a more cache-friendly conflict resolution algorithm. But then again, this says nothing about the potential performance improvement unless you're able to measure it.
> I've implemented a hash map in my code. I loved the experience, and I know more about computers and hash maps now that I've done it.
Yep, everyone at least once should implement a hash map. Hash maps are so fundamental and so often used that it really pays off to understand what makes them tick.
> And my hash map was faster than the one in the C++ STL last I checked. I haven't benchmarked it against the Rust one, though.
The one in C++ is very famous for being really slow, so this is a low bar to clear. (:
Not sure if someone directly compared Rust's hash map to C++'s, but Rust's hash map is based on Google's SwissTable, so you can probably assume it performs roughly the same. Here are some graphs: https://youtu.be/ncHmEUmJZf4?t=2093
> The one in C++ is very famous for being really slow, so this is a low bar to clear. (:
There's nothing wrong with the C++ hash-map. It's a different trade-off which makes perfect sense for something that is part of the general-purpose toolbox. See my response above for more details if interested.
Rust seems nice when you see someone like Jon Gjengset writing it [1], however when actually writing it yourself you discover that your errors have errors which only show some kind of alien fishing when you Google them. Perhaps ChatGPT could help here, it's quite useful/pleasant for trivial quizzes, however I asked it recently how to handle a file upload using actix-web and it hallucinated some really nice methods, too bad they aren't in the code base.
Just a random recent example: call an async function for a side effect without caring what its result is, Rust yells at you that the result must be handled "`#[warn(unused_must_use)]` on by default", so you'd think I would just annotate with `#[allow(unused_must_use)]`, but no way, you must store the result in a _ variable: "let _ = my_async_fn().await;". It's a great language, it's just ugly, sorry, not sorry.
You do not have to use the result of an async function to avoid getting a warning. You have to use the future returned by the underlying sync function (I.e., you have to invoke “await”), in order for the async code to actually run. That warning probably caught a real bug.
No bug caught, just a mild annoyance. In this setup I have an async function `index` returning `actix_web::HttpResponse` which is to be used for an actix-web route `.route("/", web::get().to(index))`, inside `index` as a side effect I want to call another async function `write_metadata` which returns `Result<bool, anyhow::Error>` but I don't actually care about the result. I can play with the return type of the `write_metadata` async function, making it return a custom error or even a `HttpResponse`, or I can use `let _`, anyhow, lots of work for something this trivial. As said initially: seeing a fully completed Rust project, syntax ugliness aside, it's rather interesting, building one gradually, trying things out, means plenty of pain from meaningless/context irrelevant language quirks.
That’s because Result is must_use. Nothing to do with async.
In that case, yes, let _ is the usual pattern. But it’s rare that I ever actually want to use that. Typically I either return the error upwards or just unwrap. But if you truly don’t care about whether the operation was successful you can indeed ignore the error.
There are plenty of pain points with Rust but I don’t really think this is one.
Btw, the article you linked argued against the claim that Rust’s syntax is ugly, and instead claims that Rust’s noisyness is due to its semantics, largely in service of performance (which is why production C++ code is often similarly ugly).
Nitpick: _ is not a variable, it's a wildcard pattern that means "ignore this value". It can occur in other places like tuple deconstruction and the likes.
Nitpicking the nitpick: _ is a wildcard pattern when stated in a match as in [1], otherwise it's indeed a placeholder as a standalone, but also a special character when using it in the normal code flow to allow for unused variables as in `let _foo`. Asked even ChatGPT about it (they must add permalinks for the answers), they answered: "In Rust, the '_' symbol is a special value that can be used in a number of ways. Here are a few common uses: 1. As a placeholder for unused variables. 2. In pattern matching. 3. In iterators."
Although `go vet` is nominally a linter it's probably more accurate in relation to other languages to think of it as `-Werror`. Everything it reports should be considered a serious issue, and it runs by default during `go test`. They haven't been moved to the compiler proper out of back-compat concerns (but lately even some of those are being overridden for very pathological things like int/string conversion).
I also have mixed feelings about Rust. It gives me too many flashbacks from Scala. Complex types, a lot of generics, same elitist attitude. I just think it will end up the same way as Scala. Few years of hype and now we have plenty of unmaintainable codebases from wannabe functional zealots.
I like other communities experimenting new ways. And the Rust lesson will not be forgotten even if Rust will be replaced by something else. However what I do not understand is, given the enormous code base written in C, the fact it will be used more in the future regardless of Rust and other alternatives, is why we don't fix C, at the same time. Create a standard project, within the ANSI C, to write a new, solid, modern standard library for C. Change the language behaviour so that the default compilation target is full of defined behaviours for all the common edge cases, and you can enable an "UB mode" only for speed and if you know what you are doing. And so forth.
I suspect the problem is as always, political. The standards committee is dominated by compiler vendors, who at this point are desperate to cast C in the dustbin of history, or at least put it in deep freeze, and are eager to push their other favoured languages like C#, C++ and so on. Therefore the committee has decided to please all parties by pleasing none, i.e., keep C stagnated, with only minimal changes that everyone manages to agree on, essentially relegated to embedded work and legacy code maintenance. And as long as C remains under ISO's grip, this won't change, sadly. C needs a rejuvenation and ISO is the last place to make it happen.
Absolutely true. C might have its own way of evolution to Modern C with quite a good standard library and mature features, but people who stay behind C++ don't want this, and even though some of them behave really aggressively if some languages like Rust or Zig or Modern C (unfortunately this is will never ever happened) try to replace C++ or take even a little piece of the pie.
C++ is not a bad language at least because lots of amazing soft already written on it, but some people accept it with all of the myriads of complex rules with their own subset and some people totally disagree with this and just want Better(Modern) C with plain and simple rules without hidden complex things which happened behind the scene, and counterintuitive features.
About Rust is everything cool except sometimes it doesn't allow you to write correct code and push you to do things in ways that you don't like. But we are programmers and some part of us is a painter :)
This would be really cool. If there would be a more modern version of C, made by C standard committee. Without UB and with small, but robust stdlib. It would have to stay compatible on ABI level, but source level I think we can skip.
For me Zig looks like something that could become a new C. They aim for great C binary and source compatibility (both ways - called and callable from C). It’s planned to be small and lean. And avoid preprocessor and UB as much as possible.
Both suggestions require companies to invest a lot of their resources (money, time) on something that seemingly is not particularly interesting to them. The former suggestion being somewhat realistic but the latter one I think not very much so. C++ was/is what the companies chose to put their money in to.
I think your first point on limiting productivity is true only early on (or at least it was in my case) and when working alone.
I pretty confident that this would go away if you write Rust for a few weeks.
I remember when first learning C, I struggled with some things too, but then they just clicked. It's probably similar when learning any new language (especially if they add something novel)
I've been writing Rust code since 2013, and did what was probably the first large-scale production deployment of Rust code in 2014, at OpenDNS. Wrote quite a few crate and apps since, and started to heavily advocate for it.
Here's why. In spite of having quite a bit of experience with many functional and imperative languages before, and picking up new languages used to be a seamless experience. But Rust turned out to be an extremely frustrating experience. I was feeling like I couldn't tell the compiler to do what I wanted it to do any more.
I took it as a challenge, didn't want to give up.
And when the first non-HelloWorld app actually got to compile and work (it was probably IPTrap 2, that implements a userland TCP stack), there was this feeling of accomplishment that I couldn't help myself sharing with the rest of the world.
Fast forwarding a couple years... I don't enjoy Rust any more. I still maintain Rust code, but it's a pain, not something I'm having fun doing.
First, memory safety in Rust is overrated. What it actually brings over GC'd languages is that two threads can't share the same pointer at the same time. Nothing else. If I'm not using threads, it doesn't matter. It's not any safer than PHP, Go or JS, but is far more complex. I won't provide memory leaks nor deadlocks either. To some extent, it actually encourages them (I'm often blindly adding Arc, Box, and Mutexes just to silence the borrow checker, not because they are actually needed).
Next, readability is not great. Too many sigils, and the language got more and more complex and less and less readable over time. I'm frequently having a hard time reading my own code after a while. Even more with other people's code. It often feels like people try to maximize the number of language features being used, so that the source code looks impressive, rather than keep the code basic and simple.
Finally, the ecosystem is very unstable. 3rd-party crates (even essential ones such as for error handling, due to limitations of the standard library) are constantly deprecated, abandoned, or having braking API changes. I gave up maintaining some projects, and keep using abandoned (even with security issues, such as old versions of hyper) with others, because the maintenance cost is too high.
After using almost exclusively Rust, choosing Go for a new project (dnscrypt-proxy 2) was enlightening. There were probably runtime bugs that Rust could have detected at compile-time, but overall, productivity was way higher. Making a change is straightforward, I don't have to think about the implications on the borrow checker. The compiler is fast. Performance is fine, and I can spend more time optimizing the actual algorithms than fight the compiler. Go tells you at runtime, not at compile time, if locks have been forgotten. But otherwise, memory safety is comparable to Rust. The runtime gracefully catches out of bound accesses to slices, null pointer derefs, etc.
I also still enjoy writing C code. It's good to have some freedom and control. But C also has rough edges. UBs, portability, minimal standard library.
Zig fixes many issues that C had, and is the language I really enjoy the most now. It's fast, portable, easy to learn, easy to read, has proper error handling, strong typing, catches out of bound accesses to slices and does checked arithmetic, and, overall, is a joy to use. Not to mention its seamless integration with C.
It doesn't have temporal memory safety, but, like Go, Zig's simplicity helps me reduce logic bugs, and focus more on algorithms than their implementation, overall resulting in better code.
Thanks for sharing your perspective. I think, broadly speaking, that code maintenance is no fun. Thus, languages with a huge existing code base are often dreaded (Java, C++).
Rust profits from "rewrite it in Rust", since it is more fun. But so does Zig.
Still, I am worried about the ecosystem, since every time I pull a bigger crate through cargo, I have to think of npm and the sad state of JS ecosystem. Also, I have a small, rewrite from python project at work and I am also worried whether it will be easy to upgrade in 2, 3, 5 years? If I write it in Java, it certainly will...
Fully agree on readability. It is not that Rust is unreadable, but a lot can happen in the background and you have to look hard for small sigils, presence of semicolons and blocks.
I think that Rust is great option for writing new software where having any kind of automatic memory management isn't an option (or even if possible is a lost battle trying to convince others), like kernel code, drivers, type 1 hypervisors.
Otherwise there is Go, Haskell, OCaml, Java, Kotlin, Scala, Clojure, Lisp, Scheme, F#,...
> 1. In order to reach memory safety it add abstractions that don't make programming simpler, but harder.
Another possible conclusion is that your program design is just not as simple as you thought it was, and Rust is pointing out why. This drives you to make it even simpler, which is arguably good.
If you don't like Rust, try a Lisp. You're probably looking for one that can compile fast code and has good, easy to use C FFI support. Gambit, Chicken, SBCL, CCL, and ECL are all good choices here, with Guile 3.0 also potentially a contender.
If Redis works then the proof is in the pudding, I guess. But as an average/below average programmer I’ll stick to something more modern when I need that level of control.
If you are part of development community, you also get to read other people's code, use libraries, and so forth. When I open a C file it can be written without care certain times, but C is so self-evident that it's hard to don't understand the code after some superficial reading.
I think C is misleadingly easy: it's easy to reason about, say, a single function body since there's only so many things you can even write in C.
But when you start composing things together and juggling the lifetimes of various things, the complexity balloons and it becomes a lot harder to understand "spooky action at a distance"-type things. You might argue that with good structure and discipline it's still not difficult, but the assumptions are all implicit and you have to get into the mind of whoever wrote it in the first place to understand it. And I write a lot of C and still find it tricky when I have to go look at some random C codebase!
> I write a lot of C and still find it tricky when I have to go look at some random C codebase
Absolutely. C Code is not necessarily easy to understand. Not at all.
But the C parts itself are? In C you seldom wonder what C does, you wonder what the C code does.
If you work like OP likes, single person projects, small enough to keep almost completely in your head, I can see how C is especially charming.
You never wrangle with C, only with your own code.
I agree, but what I said has very little to do with that. What you says means that poorly written code has unexpected bugs. But you understand what it does, easily.
This leads to the same problem as in the C++ world: a fragmentation of the library ecosystem because of wildly different opinions on what "proper C++" actually is.
One example: There's a lot of JSON parsing libraries across the whole spectrum, from using the latest C++ features, or exceptions for error handling, or various subsets of stdlib types, etc... down to essentially being "C with namespaces".
But usually the most controversial features are:
- use of exceptions
- use of RTTI and dynamic_cast
- use of stdlib types which allocate memory under the hood
- ...or even use of any stdlib types
- has boost or similar complex dependencies
- use of 'modern C++' features that explode compile times (like the range stuff)
...etc etc... when you look at C++ libraries written in the game development or embedded hemisphere, those usually use an entirely different C++ subset than the more academic "modern C++" crowd.
The deep-embedded/real-time compatible features I'd agree, that's really a different ecosystem, but that is the case in every language (assuming the language can even be used in such environments). Other than that I don't see it come up very often in practice as something that makes or breaks a library choice.
Rust has explicit subsets of its standard library:
* core, whose only dependencies are six symbols (memcpy, memcmp, memset, strlen, rust_begin_panic, and rust_eh_personality)
* alloc, which layers on top of core, and adds the ability to allocate memory
* std, which includes all of the operating system specific things
But most importantly, you can declare that your code doesn't require std and alloc with an annotation, and there's a way to tag these crates in the package manager as well. Additionally, cargo has a "features" feature, so you can say "please give me the no_std version of the library" and get what fits within your needs.
My team at work is working purely in the 'core' subset, and we're able to easily find and evaluate open source projects that will work for us, because of this.
Oh, I should also mention: while Rust doesn't have exceptions, there's also a way to compile your program so that panics abort instead of unwind the stack, similar to -fno-exceptions being passed when compiling C++ code. There is also a setting you can include so that, if, for some reason, you require the unwinding semantics, if someone tries to build a project with abort semantics and your program, it will not allow it and let you know why. I don't think I've ever seen this annotation on a package in the wild, because since they're not used in the regular course of programming in Rust, virtually every open source library does not use them for important semantics and so therefore works with either setting.
One is a cultural matter: A design idea that no longer actively attempts to produce a language with few orthogonal features that work well together. The second is a side-effect: the more features you have in a language, the more complex new features will be to accomodate all the past cases.
I like C as well, but I never kid myself into thinking that discipline and documentation can replace compiler restrictions. I've written large codebases in C, sometimes alone, sometimes in a team, and despite DECADES of experience and meticulous attention to detail, I STILL find the same old memory errors in my own code.
I mean this is demonstrably untrue: SEL4 is written by mere mortals, in C. It's safer than rust, there are formally verified security guarantees that include and go beyond the memory safety of rust.
It is the case though that c is an incredibly shitty language to do this sort of static analysis on because for example it's easy to do shenanigans like lexical macros that make the static analyzers job much much harder.
seL4 also has the whole Isabelle/HOL theorem prover to verify its properties, so it's not quite right to claim it's written in C. If you can claim seL4 is "written in C", you could equally claim that if the Rust compiler emitted C, you're writing a C program -- after all, a static type checker is just a shittier theorem prover.
This is like saying <insert language with docstrings> is not actually writing in <lang> because you need whatever engine's target language (md, XML, whatever) to compile the docs.
That comparison is nonsensical and you know it. You can delete all the comments and have the same guarantees about what the code does (none). You can’t write a kernel without some formal verification tool and then claim that it adheres to the specification.
Oh, I see. You said that it is “written in C”, and then later denied the claim that it is also written with that formal verification language. If you delete those annotations it will still adhere to the specification,[1] but it’s still false that it was purely written in C—your statement that “there are formally verified security guarantees” in fact relies on the existence of that proof code.
But fine. With that kind of dishonesty I could claim that the Linux kernel is written in nothing: I have compiled it and deleted the source code. Look ma, it’s no-code kernel.
[1] Kind of a bummer though that you cannot change any of the C code any more now that the verification code is gone.
Jesus Christ. It's not like most c programs arent already written in another language like make or cmake. By your standards none but the most trivial of hello world a.out c programs are c programs.
Macros are not such a big deal: static analyzers do not work on the source code directly, they work on some intermediate representation usually (often the LLVM IR, for Ikos and Clang SA for example) or parsed source code. Either way, the pre-processing has been done when the analysis starts.
Funky macros can still make static analysis more complex by introducing layer(s) of indirection of course.
You can make any code in any language "safe" when you throw enough resources at it. That's not an argument for safety, nor a rebuttal (unless your goal is to prove someone wrong on the internet).
It's the exact opposite of "you cannot make language X safer", only if you define that as "given enough time and resources, you could make a proof that your program is correct".
But that's not a safe language; that's a safe work. ANY programming work - regardless of language - can be made safe via such expensive means, which puts this square into the space of "argument ad absurdum". You could argue that brainfuck or assembler can be made safe via these means, but everyone would rightly ridicule you for it.
Maybe I'm crazy but "being a god" is much higher bar than "given enough time and resources, you could make a proof that your program is correct". Moreover, unlike your bf analogy: Sel4 exists, and is in use, and is used in safety-critical applications such as military drones.
it's also a very limited version of C, without concurrency, dynamic memory allocation, or goto, and you can't even take the address of local variables!
> C cannot be made safe because you are not a god.
Oh god yes. That should be tattooed on everyone who thinks otherwise. “Ha! Unlike all those other people, who are clearly not as attentive as myself, _I_ never make this type of mistake.”
To be fair, religiously running Valgrind, Helgrind, Asan plus a static analyzer will probably do more than most compilers to ensure you don't have memory issues. Probably more than Rust, at least if you find yourself needing some unsafe blocks in your own code or dependencies.
This only works if you don't stray too far away from the "typical" C. But a lot of large projects written in C will have tons of extensions and modifications making tools like Valgrind irrelevant.
For instance, I worked in a company that rolled its own coroutines in a way very similar to how they work in Go on top of many other things they did to the language runtime. Valgrind couldn't "understand" what was happening in that runtime at all and if you tried to run it would spew millions of warnings (similar problem you'd have if you used Bohem GC with your C program).
There was a way to "teach" Valgrind to understand what was going on in that runtime, but and even though we tried, at some point we realized that it would take too much effort to do so.
I'd bet that OPs home-made alloca() would have a similar effect on Valgrind too.
I was only trying to defend the article author from the accusation of hubris. If all of these tools are indeed used until they show no errors at all, not even false positives (as the article claims), and if the test suite used to run them is non-trivial, then they are likely far more secure than a typical program written in Rust or even something like Go or C# or Java (which all allow and internally use raw pointer manipulation for things like interop).
Broadly speaking, it's automated generation of test inputs to try to find bugs. For example, fuzzing a parser might involve feeding it random inputs and watching for crashes, hangs, etc.
Fuzzers frequently also use instrumentation to try to figure out inputs that will hit more code paths in a more intelligent manner.
Nothing is stopping you from running all of that on rust code though. In fact, you probably should just for sanity. Compilers have bugs. Libraries have bugs. At the lowest levels there’s unsafe somewhere anyway. Rust doesn’t remove the need for those things as good engineering practice. It just makes sure that the danger of failing to do so is minimized. Also, when there is a failure, it’ll be limited to very specific code paths (barring compiler bugs which themselves aren’t thaaat common all things considered).
Does Rust stable support the commonly-used sanitizers (ASan, TSan, UBSan, etc.) though? The last time I've checked it was still a work in progress
(https://github.com/rust-lang/rust/issues/39699).
It’s not stabilized yet but it runs fine on x86_64. I don’t really see why it not being in stable is a huge blocker. It’s a bit inconvenient, but you can always rebuild your project with nightly, no?
That's beside the point. The OP was claiming that the article author is showing hubris, and I was just pointing out that, assuming their claims about what tools they use are all true, they are in fact showing more care than any project which relies only on the compiler and runtime system for memory safety.
Most of those tools check runtime properties of your code. If your test suite is perfect, that gets you quite far. I don't know of many nontrivial projects with perfect test suites.
Without a good test suite, there are often security issues that don't depend on memory safety violations. Also, I did mention a static analyzer among the tools, which will likely be able to do analysis quite similar to the Rust borrow-checker (but with an order of magnitude more false positives, which the OP claims they are fixing anyway).
This! If you really care about security you should educate people about how to use available tools to analyze C instead of relentless Rust evangelism.
People arguing that "you can not ever write safe C" might also come to the conclusion that one "unsafe" somewhere in your Rust code potentially means that the whole code base is immediately at the same level of implicit memory safety as C.
> People arguing that "you can not ever write safe C" might also come to the conclusion that one "unsafe" somewhere in your Rust code potentially means that the whole code base is immediately at the same level of implicit memory safety as C.
No, the forced use of "unsafe" in Rust for some kinds of safety mechanisms to be disabled makes the code to audit easier to locate and reason about.
IF the code in the "unsafe" block does not actually isolate the "unsafe"-ness THEN conclusions can be drawn about the rest of the code.
Contrast that to C where the code introducing unsafeness, UB and unsoundness could be basically anywhere instead of a convenient "rg/grep unsafe" away.
I don't agree at all. Developing in Rust (or other memory-safe or mostly-memory-safe languages) is going to be orders of magnitude easier than writing things in C and then running through several dynamic and static code analyzers and quieting the mountain of false positives you'll see with those, plus the massive test suite that you need to make any use of them.
I was just pointing out that, in the case of the particularly diligent OP, he is probably getting an equivalent level of safety - for much more effort though.
To me writing C is orders of magnitude easier because I almost instantly can see in my mind the resulting assembly code generated by the C code. In comparison Rust feels very opaque (e.g. A simple writeln expands into a rather large macro and who knows what the compiler is doing with that). I'm sure with more experience you can get better but why bother.
Even with just static analysis and some sane coding conventions you can get C on the same safety level of Rust. I see Rust as a huge waste of everybody's time and resources and far too opinionated in many regards.
Your experience is obviously your experience. However, I don't think for the majority of people (and definitely not for myself) knowing what assembly results from my code is any way an aid in checking if it's correct or at least secure.
Either way, x86_64 assembly code is a high level language that gets interpreted by the CPU at runtime, so not sure how relevant it is to anything low level anymore.
The author doesn't seem to disagree with your view:
> The final reason is the only one that justifies my decision: my code will be rewritten in a memory-safe language. [...] It’s my own language. It’s called Yao.
> This is the only reason I could justify that decision. All of the other reasons above are excuses. This is the real reason.
> You can bet that I do not want to accept liability for a project written in C. It will be rewritten as soon as possible.
Which is fair. The author also highlights an interesting problem: Some of us will opt to not use Rust, even when we should, simply because the language doesn't appeal to us.
I love the idea of Rust, but find the syntax to be just awful, and the entire thing just overly complex. I'm not saying that Rust isn't the way it is for good reasons, but the code is often hard to read. Some of the syntax choices are in my mind questionable and serves only to have Rust programmer appear smarter than the rest of us.
Rust shouldn't try to accommodate people like me, because it will make everything worse for everyone. Instead I should look elsewhere, may not to C, I'm not that clever, but to languages like Go, Nim, Crystal or Hare. It's important that we have choices, because even the best designed language, and I do consider Rust a well designed language, will fail to appeal to everyone.
Yes, that was very good read and help moderate my annoyance with Rust. I could quite figure out if the "unfolding" or rewrite of the examples had any negative impact on performance. If it doesn't I fail to see the benefit of the rather convoluted code used in the examples.
I wrote a few programs in Nim to get a feel for the language, coming from a background in mostly Python I found it pretty easy to pick on the basic language structure.
The language is just an interface between humans and the machine to make it easier for us humans - machines aren't all the same so why should humans be?
"Some of us will opt to not use Rust, even when we should"
Nobody SHOULD use Rust.
Some may use it, but nobody should.
And because of this "should" many will not use it, especially when you consider that the language is conceptually overweight and there was already one "very popular" conceptually overweight Scala language, which now only the old people remember (laughs).
You need not be so offended, it's not even about your text.
You can use search for the next criteria: why you should use rust
This informational noise is very disturbing and forms a somewhat distorted perception of reality, because it profanes the choice of a very complex instrument (which is just a small part of the huge instrumental set). And thanks to this noise, the obvious boundaries between the apparently different "must" and "can" are lost.
Seriously. The post should've just been the "Rust Isn’t Fun for Me" section. That's the reason, everything else is just rationalizing.
To be clear, I don't mean to disparage the author's decision. Programming can be an artistic endeavour, where personal enjoyment trumps most concerns. If programming C is what keeps you motivated, go ahead and use it! Just be aware that this may make your software risky to use and rejected in many real-world situations.
The "Rust Evangelism Strike Force" isn't actually a thing that exists. From my understanding the term was created as a self-deprecating meme by some Rust users to describe other Rust users. It's not a core policy and basically all serious Rust users don't do any of the alleged activities claimed to happen. They're too busy actually writing software.
Er, it's not a formal thing, but it's absolutely an emergent phenomenon that exists. Every single discussion on HN about either software vulnerabilities or C will have comments asserting that everything must be rewritten in rust. I would believe that it's not from "serious Rust users", but someone will show up to say it.
> The "Rust Evangelism Strike Force" isn't actually a thing that exists. (...) It's not a core policy and basically all serious Rust users don't do any of the alleged activities claimed to happen.
If that was remotely true how do you explain the Actix episode?
Some over zealous Redditors doing Redditor things. That's where it started anyway. There was no coordinated effort. Probably some people from 4chan as well.
> Some over zealous Redditors doing Redditor things.
Except that this was not a Reddit problem, was it? This was a problem created by the Rust community that showcases exactly the "Rust Evangelism Strike Force" behavior we're talking here.
Would it made it a Mastodon problem if the Rust fundamentalists opted to use that to drive their relentless attacks? It wouldn't, would it?
I mean, it surely wasn't a GitHub problem when these Rust evangelists started harassing the maintainer through bug reports, which was the place where it all actually took place.
By "people threw a fit" you mean the Rust community put up a relentless harrasment campaign targeting the project author, even to the point where they involved their employer, and ultimately forced the maintainer to give up on FLOSS and abandoned the project altogether.
I mean I saw it on Reddit, guy got panned for abusing unsafe and bunch of unsafe were patched out. That's it as far as what I am aware of.
> Rust community put up a relentless harrasment campaign targeting
Don't judge community (especially one you don't know) by their worst member.
Rust community didn't all agree and made a pact to harass/dox that guy. Few assholes did.
Trying to generalize this behavior to entirety of Rust community is disingenuous. Imagine if I started saying all programmers were wife killers, because some programmers killed their wifes.
> Don't judge community (especially one you don't know) by their worst member.
The community was massively represented in that harassment campaign targeting Actix. We can still read the threads and the bug reports that fueled that shit show. Things got so bad that distinguished members of Rust's core team felt compelled to put on their PR hat to work on salvaging Rust's reputation and do limitation.
It was not bad apples. It was a hallmark of Rust's community.
I don't believe in collective guilt. If 10 or 20 or 200 assholes out of group of 30000 do something stupid, do you sentence the 30000 or the assholes, even if the rest of community has distanced from it? You are generalizing actions of a vocal few to the entire group.
Also, sure Rust people care overwhelmingly about Rust crates.
Any large enough community will statistically have some percent of "bad apples". You're arguing as if those bad apples are a majority. And not a statistical footnote.
> I don't believe in collective guilt. If 10 or 20 or 200 assholes out of group of 30000 do something stupid, do you sentence the 30000 or the assholes, even if the rest of community has distanced from it?
You mentioned Reddit. Rust's subreddit had at the time over 80k subscribers. The subreddit massively piled on the maintainer of Actix. This harassment campaign was so massive that core Rust members felt obligated to write public declarations how Rust should invest in community building and, with the Actix case as an example, whether they could actually reject contempt culture.
It was not an isolated bad apple. Rust's community is renowned for being toxic, hostile and abbrasive. This topic is underlined time and again even in HN. It's not possible to hide this fact.
I don't think people ending up on the wrong end of it particularly care if it's an actual organization or just an emergent "feature" of doing things in a space somewhat adjacent to Rust.
It was actually coined by the n-gate guy as a description of how some Hackernews descend upon a thread to advocate for Rust, typically proposing that something be rewritten in the language because muh memory safety.
The Rust community just picked it up. as self-deprecating humor.
No. The reason is everyone who works with Rust chooses to do so, because it is new and jobs are far and few, as opposed to the billions of legacy code lines in eg C++ that nobody likes but companies need maintained.
Hating everything except a particular language is an excellent reason to use that particular language. He didn't explain why everybody should use C, he explained why he uses C.
The time spent working with C is just used as a proxy for the switching costs. He's probably overly conservative here, but I'm not seeing Stockholm anything there.
I laughed a bit when I saw “memory safety” and C in the same sentence. Your program is only as good as the programmer writing the program. Having said that, it just takes one missed check to cause a buffer overflow in C.
The only flaw in the original paper was the inclusion of "Rust" as a safe language. Anyone who understands what it really does knows that you can indeed write unsafe code in Rust, unlike Java for example. Rust makes it easier to write safe code, but doesn't prevent unsafe code to be written, which puts it back in the category that should be avoided by the masses.
Moreover, Rust is extremely complex, similar or worse than C++. A programmer has to learn a lot of alien concepts that they don't need when using Java, Go, or similar safe languages.
Rust is absolutely a safe language, and in more ways than just memory safety. The only way to get unsafe code in rust is to opt in to unsafety with a keyword that can easily be checked for, so that you can decide for yourself to enforce that no code in your project uses it.
And unless you are writing a few specific types of code, using unsafe usually isn't ever necessary at all.
The complexity is also very much not an issue in practice. If you rub up against complexity in C++, you might not even know it. UB could be silently ruining things. If you rub up against complexity in rust, you were either just saved from yourself, or you are hitting part of the language that is probably undergoing improvement. But you never have to worry about complexity silently making things worse.
That's my point, if Rust is as complex as C++, it is not a good choice compared to the other languages.
In fact, many people still avoid C++ and use C exactly for this same issue, C++ is too complex to use. These people who use C will justifiably have the same argument against Rust, it is too complex, and migrate when needed to something else like Go.
If a less complex language meets your requirements, by all means use it. But I still have no idea how that is relevant here. C doesn't give you safety and less complex languages don't give you performance and control. That's why comparing rust to c++ is useful.
But the way I think the higher complexity of rust over c is also not a big deal in practice, for much the same reason as I gave for rust vs c++. But that is also irrelevant here.
I feel I have to develop an economy of effort in C because there usually isn't a dumb and easy way to do things - this pleases me.
C has a mental model that seems relatively simple - I feel I can reason about what's going to happen to a greater degree than higher level languages.
C++ is the worst IMO because it has lots of abstractions but they don't make programming simpler because you pretty well have to understand the low level aspect of what they're doing to get performance - so the complexity is much greater.
Rust seems to put a lot of difficulty up front which is of no value in the kind of typical short program I write most of the time. I just didn't enjoy it. If I had something complicated to do I might eventually appreciate it.
The more high-level the language the less one tends to understand it and that's unsatisfying.
Having said all of that though, I tend to "just write python" whenever I can. If it's a throwaway program one doesn't need more and if it turns out to be necessary to interface to some library or other I know C well enough to sort that kind of thing out. I use threading.....never - at least never by choice.