The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true. I’ve learned for example quite a good chunk of Rust, implemented a project (actually a rewrite from C++) and now I’ve mostly reverted to C++ and Go. The only reason that I’d use Rust is if I had to ensure that a program is memory safe (e.g. it’s planned to be handed over to developers unexperienced with C++, it will be heavily changed in the near future, etc).
It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need. The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C or Go and I wouldn’t say that it’s hard per se to learn, as the concepts themselves are similar to C++, but there’s a lot to study, remember and categorize and map in relation to how things are done in e.g. C++. One exception is Rust’s equivalent to C++ template-heavy code which is simply not worth wasting one’s life on. Whoever writes that is either a library developer or an asshat.
Compile times are too long and what makes it worse is that everything seems to be distributed as source and has to be compiled. Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!
All in all the benefits barely outweigh the costs for me, especially since I know C++ well. Rust to me is a language for large-scale or low-level projects that need solid performance and provable memory safety, but it’s overkill for many things where one can use Go or a simplified dialect of C++.
> The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true.
Maybe we are coming at this from different angles but I disagree. I remember seeing some discussions in the C++ community about challenges with using `std::string_view` that seemed like they were discouraging it and thinking to myself "this isn't a problem in Rust".
In fact, I refactored a crate I maintain from cloning objects all over the place to using references. I looked at what i did with my C++ hat on and realized that I'd consider myself irresponsible to do the same in C++ without a *very* high bar to justify it, not just because of the cost of the global analysis to ensure it was safe but the fact that that global analysis would need to be repeated on every subsequent change. Being in Rust, it compiled and I knew I was good. I even had an idea for reducing even more allocations that I thought was safe (20+ years of C++ experience, 10 in life-or-death mission-critical software) and the compiler found a case where I was wrong and the approach doesn't work.
The keyword is overall. One wouldn’t typically pick a programming language purely based on how well it handles references.
Generally Rust does memory safety better than C++, that’s painfully clear as soon as one uses the language and compares it with what happens in a C++ project with average developers. But memory safety is just one dimension of evaluation.
> The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C...
I think this is only true if you're already familiar with C-family languages (C and C++). As someone who came to lower-level programming with a JavaScript/Python/PHP background, I found Rust significantly easier to learn than either C or C++ (C was easy to do hello world, but jumped to WTF-level complexity as soon as I needed to interact with a 3-party library).
Let me put it this way: would you trust a developer who was new to C or C++ to write secure code that's free of memory safety issues, undefined behaviour and multi-threading race conditions? You can do that with Rust, and IMO that's a huge win.
> I think this is only true if you're already familiar with C-family languages (C and C++).
This comes up a lot. A lot of people learn C or C++ in school, so the distinction between learning those languages and learning programming in general gets blurry. Being full of UB that "works on my box" also makes things blurry: If you don't know how to write sound programs, have you actually "learned" the language?
I think learning C or C++ takes 2-5 years. Many people forget that, because it was mixed in with learning other things. Many others quit the learning process partway through without realizing it. Rust forces you to actually learn Rust to write Rust, which can make it seem like there's more to learn by comparison. But if you restrict yourself to the safe subset, I think there's actually much less to learn.
C++ and Rust are very deep multi-paradigm languages, with many features that try to cover a lot of domains - that’s why they are complicated. Sure, C++ has the decades of backwards compatibility and weaker memory safety, but in the end it’s clear they belong to the same difficulty class, even if C++ is likely harder to learn.
Provided they never use unsafe blocks and the third party dependencies have been inspected for code safety invariants when called from safe code with bad parameters.
I'd argue that even without the memory safety and outstanding tooling, Rust (at least no-allocator, embedded Rust) maps 1:1 with C and C++ with nicer syntax.
Some examples:
- No DRY between headers and source files
- Arrays are explicitly described as such in type signatures, ie not as pointers to the type they contain
- No pre-processor
- Cleaner, more explicit pattern matching syntax
- No prefixing all constants/structs/enums with the module name
- A struct or enum is declared once, with its name; ie no _t, _e, _s suffix and name duplication
- Type inference
- Explicit handling of enum variants
Why is a preprocessor bad? I do tons of platform level programming. Let’s say I’m doing Metal. There is #if ISO, #if APPLETV, #if MACOS stuff around the code. Sure, I could over engineer by trying to pass in some specialization functions but it can get extremely tedious and ugly to deal with. There’s a time and place for both.
If I recall correctly, C# didn’t ship with one but it was added. For one it makes it easier to use the standard build system (just compile all the files referenced) rather than have to use an outside build system that knows when to include linux.xxx and when to incluide windows.xxx etc.
Of course I’m aware of some of the drawbacks but I’d prefer with than without.
Rust has equivalent functionality but it works on syntax trees rather than token streams, so you write `#[cfg(target_os = "macos")]` as an attribute rather than wrapping an arbitrary chunk of text in `#if`/`#endif`.
Didn't know that! I've been translating several C and C++ (mostly C) codebases (embedded), and haven't seen those. Probably because C, and older C++ specs / design patterns.
Pattern matching is much more powerful in Rust than in C++, even with Boost libraries. Type inference is likewise more powerful, as you can overload on return type.
"The number one problem Rust has now is that it’s overall not significantly better than C++"
This might be true for your specific use cases, but I've worked on reasonably large projects in both Rust and C++ and the fact that I don't have to chase various issues related to use-after-free, returning local variables, dealing with external libraries or general metaprogramming is definitely causing me to mark Rust as significantly better.
As always, if you're a C++ expert you'll bound to disagree, YMMV etc.
> It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need.
I think this is why you don't think it's significantly better. I use JS and Python for most of my work, but when I want something that must do threading correctly or is performance bound, then I go to Rust over C++. That level of safety is the critical value for me.
(I also find it significantly easier to compile Rust than the typical C++ build chain, especially when doing cross platform work, but that's more a compiler than language issue.)
I went through pretty much the exact same process several years ago, coming from the same place of deep C++ expertise. Rust has some cool stuff, but it also requires learning a ton of new practices, and C++ is getting better fast enough that it just doesn't seem worthwhile to switch.
> Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!
Being able to share code easily is a good thing, and it's one of the biggest draws of Rust compared to C++. You will see the same thing once C++ modules and package managers take off; in fact, if you don't, that means that C++ modules have failed.
> and what makes it worse is that everything seems to be distributed as source and has to be compiled.
You can distribute a binary shared library written in Rust, but make sure you're using a C-style public interface to preserve ABI stability and make it accessible to code written other languages. It's generally feasible to design such an interface and build Rust-friendly wrapper code that can be included in client projects - there are crates that will specifically make this easier. C++ punts on the whole ABI issue and is now facing ecosystem-wide breakage as a result.
> Every file in the tests directory is a separate crate, compiled to its own executable. This is a reasonable decision, with undesirable consequences:
Other problems with this:
- You have to link each test file which can be slow
- You get less parallel test running because you've broken things up into smaller parallel batches. cargo-nextest is a proof-of-concept that shows this problem [0]
- Fixture initialization reuse becomes trickier
Projects like cargo explicitly combine all integration tests into a single binary. I'd love it if we explored implicitly rolling everything up into a single test binary except for any explicit test targets defined by the user.
FWIW, I tried it on a rust project here, the difference was in the noise range only; that way I knew that I was rather IO limited (linking produces lots of IO) and also that mold benchmarks _may_ potentially overpromise a bit much, at least when applied at my use case.
Ada has a set of High Integrity Restrictions that define pragmas that turn off exceptions:
"No_Exceptions - Raise_statements and exception_handlers are not allowed. No language-defined run-time checks are generated; however, a run-time check performed automatically by the hardware is permitted."
"If you disable exception checks and program execution results in a condition in which an exception would otherwise occur, the program execution is erroneous. The results are unpredictable"
It would be nice to have an overall scorecard for a library. It would show a bunch of stuff related to the crate: existence of unit tests, fuzz testing, and measures of the test coverage, if the code passes clippy checks, which edition of Rust, and such. We could also grade crates by how many open tickets they have which haven't seen a response, etc. This wouldn't be intended to shame the library maintainers, but more to indicate which projects are active, and which ones have issues fixed promptly.
It would be easy enough to also scan the source for sources of panics.
Sometimes to have the assurance means in some specific code, not always: the code is entered via some entry point, and guaranteed to reach one of its exit points without hitting a panic.
I think it's a property that you could validate via static analysis in relatively strongly-typed languages. I agree it would be impossible for Python, just because everything can change at runtime.
- System.Diagnostics.Debug.Assert (on debug builds)
- Envinroment.FailFast (this is precisely what panic! is usually used for)
- void BlowUp() { BlowUp(); }
But, in reality, any unexpected Exception: how would you properly handle an unexpected NRE or index out of bounds? Either swallowing the exception (bad) or aborting.
Exceptions can be handled, so in rust an "exception" is a "Result<T,U>". A panic means the program terminates, guaranteed. This allows us to ignore hugely difficult edge cases when interacting with hardware / OSes and still ensure the code meets safety guarantees; if the code isn't executing it cannot invalidate any invariants.
Rust “panic!” macro, though, is more of a gentle abort (I guess it's like an uncatchable exception, but that qualifier so radically changes the nature that I can't see it as really the same kind of thing.)
"Crystal has a wonderful and extremely ergonomic macro system that Rust could learn from, one that doesn’t require ad-hoc reinterpretation of language tokens and that integrates seamlessly with syntax highlighting in editors".
What about zig, where there is no macros per-se, and you just write zig code which is then interpreted by the compiler...
Author here: I haven't written any Zig, but I've read a little bit about `comptime` (which I thought was very cool).
I like compile-time programming, but I also recognize that it's programmer catnip: we love it because it lets us be clever, including in ways that aren't conducive to maintainability. That's a big part of why I like Crystal's macro system: I think it does a good job of balancing readability, expressivity, but also constraining the programmer away from compile-time gymnastics.
But like I said, I have no Zig experience, so it's very possible that Zig has much better ergonomics than I'm afraid of!
I’d like to add one: There is too much control at the language level between static and dynamic dispatch. Slimming down your binaries by using dynamic dispatch requires significant code changes when it shouldn’t necessarily have to.
I don’t know what I’d propose instead that wouldn’t be just as bad, but I think it’s a wart.
Rust initially didn't require `dyn` prefix for dynamic dispatch (trait object types), but the lack of syntactic distinction was confusing to users. Rust is low-level enough that there is a semantic difference which one you use (Rust calls it "object safety"), so you can't just pretend there's none.
Rust is also very focused on performance, and Rust users do care about making such trade-offs consciously. You care about binary size, I may care about inlined autovectorized code.
> Rust is also very focused on performance, and Rust users do care about making such trade-offs consciously. You care about binary size, I may care about inlined autovectorized code.
Everyone can agree with this. The question is whether this ought to be a property of the target platform or whether this ought to be a property of the software itself. Rust implicitly takes the stance that it is a property of the software.
Suppose we had a chip today that runs poorly due to icache thrash, so you rewrite portions of it with dynamic dispatch. Then someday we get a chip with an insanely large icache that would benefit from static dispatch. You rerewrite it. Or today if you work on embedded linux and want to use a rust library whose primary users are on powerful servers.
I will concede that I have no idea whether it’s possible to come up with a reasonable abstraction for this, because you certainly know more than me.
There are already plenty of architectural differences that change optimal trade-offs in the code, e.g. memory speed affects lookup tables vs recomputation choice and inlined data vs pointer chasing, branch prediction cost affects conditionals vs branchless/redundant code, data layout needs to be adjusted for SIMD and cache line sizes, memory hierarchy and core topology limit when it's beneficial to parallelize algorithms, and so on. These trade-offs don't even require oddball futuristic CPUs, and already vary significantly even within existing CPU families.
There are some languages that focus on "what" and magically optimize "how" (ISPC, Halide, and good'ol SQL), but for better or worse, Rust has chosen the "portable assembly" angle and micromanagement of everything. The upside is that you have very few surprises and "performance cliffs", and rest assured that if something is slow, you have enough control to fix it (as opposed to e.g. JIT-based language).
I'm not, because of the object slicing problem. Value types and inheritance don't mix well, and the way Rust expresses inheritance with explicit "dyn" traits solves the problem neatly.
One of the strengths of Go and Java is the ease of writing tools. C++ on the other hand is a mess since it's hard to look through templates (concepts probably help) and macros (good luck).
Not sure I agree with this. The relative similarity between static and dynamic dispatch in C++ is pretty confusing (to me) (edit: as in, 'virtual' vs non-'virtual' methods).
I also wish they were more similar in both C++ and Rust. I find that most programs get more dynamic as they get bigger, get more usage, and get more extensible ... but the tendency is to want them to be as efficient as possible at the beginning.
OK there is some static dispatch in regular functions with respect to argument overloading, e.g. f(int) and f(char), or obj.f(int) and obj.f(char).
But I think what is more commonly thought of as "static dispatch" is various template programming patterns, as in the blog post, which are more flexible, and has equivalents in Rust AFAIK.
Overall it does feel like there are too many similar mechanisms that look extremely different!
Would a regular non-virtual non-template method be considered single dispatch? I am pretty sure this is correct but I've never actually thought about this until I read your comment!
On the IntoIterator topic: I do think it was a mistake to `impl IntoIterator for &[mut] T` for the collections. I think it would have been better to just standardize on `.iter[_mut]()` instead. Would have avoided the whole "IntoIterator for arrays" issue and would be more explicit.
I'm just relearning Rust after first using it in the pre-1.0 days. Predicting what kind of iterator I'm going to get in what circumstances has definitely been a stumbling block for me. Sometimes it makes sense in retrospect (String::chars() must be producing copies not references, because it doesn't hold data as char internally, duh), but it is an area of the language where I frequently get unexpected compiler compiler errors.
For me, the reason I like an explicit `.iter()` is because its more refactor-friendly. If I have an existing loop over `&collection` but want to call an iterator method, I have to switch to `collection.iter().enumerate()`.
Another thing that would have avoided the whole 'IntoIterator for arrays' issue would have been implementing IntoIterator for arrays. What benefit would your version provide over the 2021 edition as-is, and why would it be worth the major consequence of fracturing the iterator system into T, &T, and &mut T, as opposed to &T and &mut T just being special cases of T?
The whole IntoIterator for arrays issue was that there was existing code using IntoIterator of the slice deref, which was incompatible with an array IntoIterator impl that would take precedence. A compiler hack was introduced to hide the array implementation on older editions. Requiring `.iter()` to get a reference iterator install of just doing it automatically would have avoided the need for a hack.
(2) `&[T]` (equiv to `slice.iter()`), `&HashMap<T>` (equiv to `map.iter()`)
(3) `&mut [T]` (equiv to `slice.iter_mut()`), `&mut HashMap<T>` (equiv to `map.iter_mut()`)
All I'm saying that `IntoIterator` should only be used for owned iterators, and it should be standard to provide `.iter()` and `.iter_mut()` for producing reference iterators. That is, only the (1) impls should exist, and the (2) and (3) impls should not. I don't understand how that requires it to act differently from every other method.
Then I misunderstood you, but now you have driven a fence between types that can be converted to iterators and types that implement IntoIterator. The purpose of IntoIterator is to describe a type that can be converted into an iterator; deliberately not implementing it for types that can, just so that there's no confusion for someone calling into_iter on a reference, does not seem like a net positive. I doubt anyone would have ever complained about references implementing IntoIterator if arrays had just implemented it from the start. Like I said, the only problem that's been brought up doesn't need a solution other than the one that's already been implemented.
Right my point is that IntoIterator should not just mean "this can be iterated over". It should instead mean "this owned type can be converted into an iterator of its elements".
> If that sounds extremely generic to you, it’s because it is! Here are just a few of the ways IntoIterator is used in the wild, using a generic Container<T> for motivation:
> • For producing “normal” borrowing iterators: &T for T in Container<T>
> • For producing iterators over mutable references: &mut T for T in Container<T>
> • For producing “consuming” (i.e., by-value) iterators: T for T in Container<T>
> • For producing “owned” (i.e., copying or cloning) iterators: T for T in Container<T: Clone>1
Life is too short for this kind of unreadable crap.
For panic detection or overflow detection there are static analysis solutions but I hope they get more integrated and standardized (as was attempted in cargo-verify)
Implementation is the challenging part: the standard library and community ecosystem is full of legitimate uses of panic, and there are instances where panics might appear in the source code of dependencies but not in (optimized) builds.
Yes, but the whole point of `no_panic` is that in this context there are no legitimate uses of panic. If that means making a bunch of parallel APIs that return results for this super-correct code then so be it.
Rust doesn’t allow emoji in identifiers, following the UAX #31 rules. It would require a significant break in someone’s tradition to get such a thing through. … quite apart from the implausibility of getting such a character into Unicode in the first place.
Presumably using `#[no_panic]` would require `#[no_std]`? Would you be allowed to use unchecked arithmetic, which panics in dev builds but not release builds?
Why would #[no_panic] require #[no_std]? There are perfectly legitimate reasons for not wanting panicking in code that uses std, and it’s possible to panic without std.
That's the issue. Rust panics on division-by-zero, Rust panics on out-of-bounds, Rust panics on signed integer overflow in debug mode. Stack overflow can happen on every function call which is also panic. Too many implicit panic sources.
The thing I've hated from the day 1 and still can not forget is the reserving of the "type" keyword. Basically every module has these ugly "tp" or "typ" or "type_" structure members everywhere but type aliases are rarely seen in comparison.
Raw identifiers can be used to get around any reserved keyword. So r#type would be as idiomatic a workaround as any. Arguably a lot clearer than the alternatives.
Rust's popularity is pure mimesis. C is fine, but it's not the hot new tech, you can't get a cool job writing C because it's "unsafe", so now we have Rust. And Swift.
I don't think this is what you should take away from this post. Rust makes me very productive as a programmer, and reduces the "long tail" of debugging that I do when I write C and C++ (which I have for over a decade).
(And, for what it's worth, there are plenty of "cool jobs" writing C/C++. I have one!)
Do you think that the reason say Amazon is writing firecracker in Rust is because they just use whatever language is popular in HN and not because it's a good choice?
Firecracker could be equally well done in many languages. It happened that the people assigned it at the time wanted production Rust experience, and were senior enough to be allowed to, despite the predictable risk of staffing difficulty after some other language steals away Rust's hipster cred. (Which one will, in time, as sure as seasons turn.)
It is a common experience for systems written in the a fad language to end up being re-implemented in a language easier to hire for. E.g. I see remarks on HN about this being a principal driver of Erlang hiring, lately.
Meaning, was it a good idea to have implemented Firecracker at all?
Every choice taken has consequences. Different choices have different consequences. Avoiding a choice has consequences.
Linux is (still) coded all in C. It is a terrible choice, but it works well enough to keep a billion phones and a billion websites operating. Language isn't fate.
Ok, firecracker could have been written in Assembly. It would have been a terrible idea. The fact that you legitimately suggested it makes me not want to engage in this conversation.
You demonstrate you were not engaged to begin with. Obviously asm is the extremest choice, which is why it was last.
But Firecracker is not a difficult project, so if somebody really wanted, they could do it in asm. Likewise, Common Lisp. Not many would choose that, but it could perform as well as the others listed before it. Some people would even choose C.
...and Dropbox can be replaced with a few shellscripts + SFTP.
Come on. Something like Firecracker has got to be both complex and extremely hard to get right. Complex because it has to deal with the ultimate in simultaneous state changes: a captive entire virtualized operating system. Hard to get right because it has a tremendous number of integration points with obscure and quirky external systems (KVM, Linux itself) at an extremely low level.
If we were talking about a startup, I'd agree with you. But given the vast resources at Amazon's disposal, I think assembly could be a legitimate option. Risky? Yes. But why not give it a shot? Assembly programmers are a dying breed, anything to incentivize people to write it is a win in my book.
Dude what sort of an argument is that? They should write it in assembly just to keep assembly programmers around? Don't they support a multitude of architectures? Are they supposed to port it for every given architecture? Are they supposed to do networking in assembly?
Other classes of bugs will take the place of memory safety bugs. For example, I don't think the recent Log4j exploit is a memory safety bug, but could be wrong.
The problem is behavioral scope: bugs will always exist, but there is no particular reason for a buffer overflow to allow someone to run arbitrary code on my computer. It should, at the absolute worst, cause an uncontrolled program termination.
Without weighing in on Rust specifically, that's a fallacy, though I'm unsure of the formal name of it: "any solution that doesn't solve all problems is equivalent to the status quo".
Nobody is claiming all bugs will go away. But the claim that new classes of bugs will manifest specifically to take the place of memory safety bugs is extraordinary and needs justification.
It's a bit like going into the wilderness and packing survival gear: the argument "you don't need a warm jacket, because if you get lost the starvation will kill you even if the frostbite doesn't" doesn't hold up.
Until that day we should make sure to eliminate the class of bugs that's #1 in all security problems -- namely buffer [over|under]flows. I'd be happy to hear this statistic has shifted towards the next #1 problem. But we're not there yet.
The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true. I’ve learned for example quite a good chunk of Rust, implemented a project (actually a rewrite from C++) and now I’ve mostly reverted to C++ and Go. The only reason that I’d use Rust is if I had to ensure that a program is memory safe (e.g. it’s planned to be handed over to developers unexperienced with C++, it will be heavily changed in the near future, etc).
It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need. The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C or Go and I wouldn’t say that it’s hard per se to learn, as the concepts themselves are similar to C++, but there’s a lot to study, remember and categorize and map in relation to how things are done in e.g. C++. One exception is Rust’s equivalent to C++ template-heavy code which is simply not worth wasting one’s life on. Whoever writes that is either a library developer or an asshat.
Compile times are too long and what makes it worse is that everything seems to be distributed as source and has to be compiled. Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!
All in all the benefits barely outweigh the costs for me, especially since I know C++ well. Rust to me is a language for large-scale or low-level projects that need solid performance and provable memory safety, but it’s overkill for many things where one can use Go or a simplified dialect of C++.