Hacker News new | ask | show | jobs
by mathieubordere 2538 days ago
Maybe I'm the only one, but I have a very hard time grasping all the functionality/concepts offered by Rust.

I really like the safety guarantees offered at compile time, and really do think that we should move away from C-like languages if we ever want to control the tsunami of security flaws, but I can't stop wondering if Rust isn't (perhaps needlessly) complicating things and scaring off (non-C++) programmers.

16 comments

I mean, C++ programmers are exactly the correct audience for a language like this - there are many other options for memory safety if you can live with a garbage collector[1]. But writing safe C++ is much, much, much more complex than than writing okay-ish C++. The way I see it, Rust has basically taken a lot of the best practices required to write sane C++ (e.g. RAII) and formalized them in a way where the compiler can enforce them. That means in order to write ANY Rust code at all, you have to adapt a lot of best practices all at once. That's not very beginner friendly, and will probably lead to cognitive overload in most - you certainly don't get the same freedom you get in other languages where you can implement something in dozens of ways, because most of those ways won't compile here. So I'm not saying they shouldn't keep working on the ergonomics and learnability of the language, but I think a lot of these complexities are essential to the task of writing sane programs while dealing with raw memory, and the fact that they have been named, formalized and checked by the compiler is entirely a good thing - and if that means the programmer has to know about them, then that's ok.

[1] Sidenote - I find it really fascinating how Rust can also use the stronger static checks to prevent things like race conditions in a way few (/no?) other languages can.

> But writing safe C++ is much, much, much more complex than than writing okay-ish C++. The way I see it, Rust has basically taken a lot of the best practices required to write sane C++ (e.g. RAII) and formalized them in a way where the compiler can enforce them.

A concrete example that I've run into recently when trying to write C++ code. I figured that, for safety reasons, I needed to make my type be move-only. I then had to spend about two hours trying to figure out why the program was blowing up. The reason was that I was reusing the variable after moving from it, and the compiler never gave any warning (even on -Wall -Werror) telling me that what I was doing was wrong. In Rust, the same situation would be a compiler error.

Yep. As much as people extol lifetimes, my personal opinion is that Rust's aliasing rules are its true golden goose. C/C++'s lax approach to aliasing causes a whole host of issues that Rust is able to avoid by being more strict.
Using a moved-from object in C++ doesn't produce any warnings because it isn't an invalid operation. The standard library types make very limited guarantees about the state of moved from objects (generally just that it remains valid to assign to them and that the object's invariants still hold), but even then it's valid to reuse them as long as you first do something that ensures they're in a known state.
Rust and C++ have rather different concepts of moving. A moved-from Rust object is entirely dead, cannot be used, and will not be dropped. A C++ moved-from object is alive as far as the language is concerned, and the destructor will still run. The move operation and the destructor need to cooperate to avoid crashing. This often adds overhead.
C++: null pointers were a mistake, so we're introducing null objects too.
clang-tidy should catch this and warn about it at compile time.

The two hours seems on the high-end, if someone's able to e.g. use ASan and the program is crashing reproducibly.

Thing is many of Rust features could probably be enforced with a static analysis tool, which a large majority unfortunately ignores.

So you either have a C++ shop where everyone is on board regarding security, with the caveat of third party dependencies, or no one cares and writes something along the lines of C with C++ compiler, without any kind of static analysis.

Relying on external tooling means it usually gets ignored if it is not enforced. After all C's first version of lint goes back to 1979.

Sadly JetBrains latest questionnaire results prove exactly that.

So having safety as integral part of the language semantics matters a lot. Defaults matter.

> Thing is many of Rust features could probably be enforced with a static analysis tool, which a large majority unfortunately ignores.

But it definitely can't be? There are plenty of open source projects (Chromium, Firefox) that develop and leverage state of the art static analysis tools and best practices. It's very clearly not enough, and the costs (built/ test time) are really significant.

Our day to day software development practices still fail short of what design by contract, MISRA, AUTOSAR, DO-178B and similar offer in terms of delivered quality.

Only with further increase in lawsuits and returned faulty software, like in other commercial areas, will companies start paying attention to QA budgets.

MISRA and DO-178B deliver more on the illusion of quality then anything else. They are desperate attempts to tame software complexity. But they don't fundamentally solve anything.
How come? They aren't perfect, but they seem to at least make Ada/Pascal out of C.
Please elaborate with examples.

Since most new cars are Internet connected and have whole hosts of complex safety features dependent on software correctness, I sure do hope you are wrong about this.

MISRA and similar standards are incredibly limiting. For example MISRA forbids dynamic allocation.

They not only make writing software a lot more difficult and expensive, they also restrict the kind of software you can write.

Do you think QA budgets would actually help here? At least the way I know it QA and development is separated, and no matter how many QA people you hire, many developers brush off QA until later.

Considering Rust shows can enforce so many things in the compiler, to me it's clear that a better compiler/language is a better way to address this problem than QA people.

Also the built in testing with cargo test makes TDD so much more attractive.

They don't, but others do. Sound static analysis tools like TrustInSoft (https://trust-in-soft.com/) guarantee no undefined behavior (array overflow, use-after-free etc.).
That's basically a reduced form of program verification, and requires a lot of developer help. You end up programming in a language that looks like C but isn't. It is not simply a matter of throwing a pile of C++ code at the tool and fixing a few errors it reports.
That depends what you mean by "a lot". The effort required is significantly less than a rewrite in a safe language. If we're talking about properties that safe languages can verify, i.e. simple, local, ones (like memory safety), verifying those in a sound static analysis tool is not hard, either.
Rust is not a collection of C++ best practices, formalized. It has its own "personality", borrowing ideas from several other languages and unfortunately these other ideas happen to be alien to most C and C++ programmers.

There was a funny discussion on the Rust subreddit, where even some language contributors have started having doubts about that complexity. One of them was trapped in his own programming language theory ivory tower, the other was trying to convince them that they are losing developers if they keep adding stuff to the language.

That discussion was a clear hint that the Rust developers don't have C and C++ programmers in mind when designing Rust. They have their own ideas about how a modern systems programming language should look like, and they're doing that. Perfectly fine, but we need to correct the misconception that C or C++ programmers will rush en masse to learn Rust.

Rust developers have actually been very cautious about integrating the 'usual' sorts of PL-theory driven features in Rust. The PL theory of Rust-like languages is still in its infancy in many ways, but it has already usefully informed the design of library features like e.g. Pin<>, as well as eased the understanding of seemingly ad-hoc language features like so-called 'internal' mutability, which - as it turns out - can be described via a remarkably simple theoretical basis.
> prevent things like race conditions

Rust can prevent all data races but not all (any?) race conditions. Related question: can you use the type system to catch a subset of race conditions?

https://stackoverflow.com/questions/49023664/could-software-...

Also, most c++ code is just...ugly. It can really assault the senses, looking at some of the template & indirection abstractions, etc. Rust just looks better, from what I've seen.
While this is very subjective, I tend to agree. All codebases I've read and worked on in C++ were eyesores in all but the simplest places.

That being said, I think Rust macros are much worse compared to C++, if you ignore templates.

Don't get me wrong, I really like Rust. I just think that it's macros make for some of the most unreadable code I've ever seen.

This seems kind of inevitable with macros. They help the macro creator write code with new, "better" abstractions, but readers of the code have to learn the semantics of the macro before they can understand what's happening. The built in mechanisms of a language are familiar to those new to a program's code but macros usually hide something significant (otherwise why have a macro).

I've written fancy macros for assembly language programming to support my own looping, iterating, argument passing etc. but I noticed that the other programmers on the team weren't interested in using them.

On the other hand, I'm so grateful to John Wiegley for his use-package macro for emacs lisp.

> readers of the code have to learn the semantics of the macro before they can understand what's happening.

They always have the alternative of reading the expanded code, which is very similar to what the author of the macro could have written by hand instead of the macro.

I had the same feeling with macros intially, but once you wrote a few of them it's ok, and they provide many more guarantees than pure text preprocessing of C/C++.
Rust is not a pleasant language to read tbh: https://github.com/SergioBenitez/Rocket/blob/v0.4/core/http/...

Clean C++ 14/17 is less cluttered.

That file looks quite readable (ignoring the comments - which again looks very readable in color syntax highlighted documentation).
I think it's the numerous single character lifetime annotations that they are referring to. I agree that this particular bit of code is somewhat hard to parse (as a rust amateur), but that might be alleviated quite a bit with some less terse naming.
Your comment prompted me to look at Rust's release history. In the whole of the history, I can spot maybe a handful of changes that made the language non-trivially harder for a beginner. Everything else either makes the language easier (by making things more predictable), or is a net neutral. Most of the language-level changes are about enabling current features to work in more scenarios, such that coming across new features tends to feel more like "oh I hadn't realised I could do this" than "what does syntax thing mean?"
This

>I have a very hard time grasping all the functionality/concepts

is (partially) because this

> if we ever want to control the tsunami of security flaws

Most focus in how the borrow checker work "against" you but that is not even the harder. Performance and how manage memory are more "painfull" in rust.

BECAUSE NOBODY KNOW HOW DO FAST & SAFE CODE.

Not ALL the time. Without extra help of the compiler your assumptions can get wrong in invisible ways...

Rust WANNA:

- See what is costly

- See what is unsafe or not

- See what own what

- See what is on heap or stack

The borrow checker is just a part of it.

From https://this-week-in-rust.org/blog/2019/07/02/this-week-in-r...

    Python and Go pick up your trash for you. 
    C lets you litter everywhere, but throws a fit when it steps on your banana peel. 
    Rust slaps you and demands that you clean up after yourself.

    – Nicholas Hahn
> Python and Go pick up your trash for you.

> C lets you litter everywhere, but throws a fit when it steps on your banana peel.

> Rust slaps you and demands that you clean up after yourself.

> – Nicholas Hahn

This is brilliant and will save me time explaining language differences. Thanks for sharing.

Python and Go pick up your trash for you, but sometimes they get in your way while they do so. If you generate a lot of trash, you might find yourself stopped quite often.

C lets you litter everywhere, but if you or anyone else steps on your trash it will tackle you to the ground. Usually. Sometimes it ignores the first 10 times and does it on the eleventh.

Rust snatches up your trash as soon as you're done with it, but if it can't reason well about when you'll be done using it, it will make you fill out a form explaining how you plan to use it. It will also slap you silly if you try to deviate from that plan.

I don't think you're alone in being put off by the complexity of the language. I think once you get over the hump things start to click. The mindset that has helped me is to say, "Ok, this syntax might sometimes look familiar, but this language is COMPLETELY DIFFERENT from anything else I know, so I can't use my usual metaphors/analogies and need to use a clean slate". Then you can look at things like Ownership, Traits, and Pattern Matching and see how the whole language is built up around a few key ideas (with a lot of subtle variations) and then it might start to click.

You need to give it a lot of time. Some of the ideas are really not familiar. I don't think Rust presents some of the ideas perfectly, but I can imagine that in 20 years there might be a whole slew of languages that borrowed ideas from rust and maybe make them appear more idiomatic.

> this language is COMPLETELY DIFFERENT from anything else I know

Rust itself borrows a lot from functional programming while also topping the story with lesser-known things like lifetimes, so no wonder it feels alien to a lot of people. In fact, the following:

> I can imagine that in 20 years there might be a whole slew of languages that borrowed ideas from rust

is actually already happening, except it's FP that's inspiring contemporary language designers (including Rust team).

To me personally even limited familiarity with Haskell probably helped a lot back when I started tinkering with Rust, it all felt more familiar to me than to average C or Python dev.

If someone can do systems programming with FreePascal/Delphi, Modula-3, .NET Native, D, Swift, Ada,...

They will be almost at home with Rust.

The biggest hurdle is dealing with the borrow checker when writing GUI code (hello Rc<RefCell<T>>), but for other kind of applications it is quite ok.

Also it speaks a lot that Ada, C++, Swift are adopting the same ideas regarding the borrow checker, even if implementation has some constraints given backwards compatibility.

In what way do you think C++ is borrowing the idea of a borrow-checker? Smart pointers pre-date Rust.
Visual Studio and clang are introducing a borrow checker in their static analysis tools. If you leave it on as part of the build you get an similar experience (note on similar, they aren't bullet proof due to language semantics).

There are a couple of conference talks about them.

Naturally smart pointers predate Rust, I used them back when Windows 3.1 was considered recent, alongside OWL.

However they aren't the same thing, introduce runtime overhead and don't prevent use-after-free, or use-after-move.

One thing about static analysis tools that seems to be easily forgotten about in discussions about rust vs other languages is that those tools are trivially ignorable. If your CI pipeline doesn't let you get a binary for a given milestone, because the code doesn't pass static analysis, unit/integration tests etc. people will just disable it for myriads of reasons not the least of which will be "management wanted it to be done by yesterday and it compiles, so it's probably ok", "it's probably false positives again [yeah, right...]", "this is the test that fails from time to time, it's probably nothing". A compilation error is a much better protection from the social pitfalls of programming in a corporate environment. So yup, you have all those wonderful tools at your disposal in the C/C++ which I guarantee you will ignore or be forced to ignore at your peril.
Using the same logic, you could argue that people will just use unsafe and shared pointers everywhere if they have a deadline and they can't get their code to compile.

This is an organizational problem, not a language / tool problem.

Even when abusing unsafe, it's hard to get away with as much laziness (especially sneaky, dangerous, indirected laziness) as you can get away with by default/accidentally in other languages when you disable their linting/analysis tools.
Actually I tend to refer to it quite often.

However one needs to see the full picture, not only language grammar and semantics.

If I want to create a GUI application today, I will definitely use a mix of .NET ,Java, with C++ for the low level performance bits, because Rust is lacking in that area, in spite of being a safer language.

So, if C++ takes a lesson or two from Rust, and helps developers like myself to keep productive, while improved the security of the whole stack, then so much the better.

And if Rust continues to improve, maybe one day Android Studio, XCode, VS, will provide an end-to-end mixed language experience, and OS frameworks, for Rust just like they do for C++ nowadays.

That makes more sense, though not actually part of the language. Smart pointers were the only thing close that came to mind for that reason.

I’ve used the Clang experimental lifetime analyzer on Godbolt, and I welcome improved tooling.

Smart pointers are a different feature than the borrow checker.

I believe your parent is referring to the Core Guidelines and the Guideline Support Library.

Lifetime analyser from VS and clang.
Do you have a link? I thought they were related, but maybe I’m behind the times!
EuroLLVM 2019 on YouTube has a talk on clang current implementation state.

Apple also demoed their XCode integration at WWDC 2019, on the talk about Objective-C, C and C++ support.

>complicating things and scaring off (non-C++) programmers.

On the contrary, Rust allow us (non C++ programmers) to use a system language without fear of breaking something. I'm a Rust developer with Ruby background and loving the language more and more.

Did you have background with languages other than Ruby before starting with Rust? Languages like C++, D, Haskell, OCaml or maybe C#?
Have you read the Rust book? I came to Rust from JavaScript, having never written more than "hello world" in C++ and found it pretty approachable.
Rust makes you think about certain things that you should be thinking about anyway when coding in C/C++ but sometimes forget about it. IMHO this makes it easier for someone who's never programmed in C/C++ to learn Rust than for a veteran in either of these - unless that person adhered religiously to certain rules of handling pointers.
Rust is a lot simpler than C++. Broadly speaking. the only 'complications' it has over C are those that are actually needed to support its patterns of memory-safety-guarantees-through-RAII. The only real alternative is GC languages, and those have their own sorts of very real complications that do absolutely scare off performance-oriented folks.

The biggest problem with Rust right now is actually its novelty and lack of maturity, that makes using it at this time a lot more problematic than it should be. But Java and Python were once "new and unproven" languages, too.

While I agree in general, just wanted to make the heads up that not all GC enable languages are made alike and a couple of them do allow for C style low level programming.

Usually the performance folks that get scared are the ones that put all of them into the same basket.

There is certainly much more accumulated cruft in C++, but you can write your C++ program ignoring most of it. You will have to deal with some of Rust's harder parts rather soon.
You can write a C++ program ignoring most of it, but when you go to work on someone else's C++ program, or import someone else's C++ code (i.e. most real-world programming) you will have to deal with it.

And when you're not working alone you will spend a lot of energy discussing which C++ subset you will use, and enforcing that.

C++ has accumulated complexity over decades, and the committee is working on making things easier for beginners. Usually it works, mine-fields like string_view aside.

Rust has accumulated its complexity over four years, and it's already comparable to C++. The thing that worries me the most about Rust, is how the language will look like in another 10 years.

> Rust has accumulated its complexity over four years, and it's already comparable to C++.

It’s not. C++ constructors alone rival the entirety of rust, and grow in complexity with every release.

You’re just so used to the unfathomable complexity of c++ you don’t realise it exists anymore.

Something which is explainable in one cppreference wiki page is not as complex as an entire language, which is described in a 550+ page book.
C++ is so complex no-one can really grasp how complex the language actually is.

C++ is not slowing down. C++ is on the verge of deprecating STL-style iterators in favour of Ranges, and modules and concepts are imminent. Template metaprogramming is being superceded by constexpr. Of course STL iterators, header files, and template metaprogramming are still going to be around, people will just need to learn all of it if they want to work on a variety of C++ projects.

STL-style iterators are not deprecated, just like LINQ and streams have not deprecated interactors on .NET and Java respectively.
Your post is kind of vague. Rust was hard to grasp, and then I learned it, and now it isn't.

I don't find anything about the language to be particularly more complex than, say, Python or C++.

As much as I like the language I actually don't think it's for everyone.

If your program is running on the server reading from a DB and producing simple JSON (like I assume most of HN's audience), rust is probably not what you want. There's plenty of more pragmatic approaches. At least I think it's not the right language for my employer's department (and it pains me to say that)

If what you want is to write code that runs on bare metal then consider Rust

> If what you want is to write code that runs on bare metal then consider Rust

The closest I was to bare metal, i.e. code that works without an OS, when I developed stuff for “small” MCUs, like Intel MCS51, Motorola COP8. Rust supports none of them: https://forge.rust-lang.org/platform-support.html

I’ve developed for Nintendo Wii, nominally there’s an OS but it’s very “thin” one, mostly statically linked libraries provided by Nintendo. Rust can’t compile for that platform either, it only supports PowerPC Linux.

I’m currently working on low-level software working on bare Linux kernel. Rust apparently supports ARM Linux, but C libraries are literally everywhere, both kernel APIs and user mode: drm, kms, gles, udev, freetype, low-level kernel stuff like tons of ioctl calls for SPI and USB I/O, wpa_supplicant, and more. That’s too much native C stuff to integrate together, using a foreign language causes too much friction.

I can think of bare metal software for which Rust is good. If I would work on x86 bare metal hypervisor, I would look at Rust very closely. Platform support is good, not much libraries are needed, and the project is extremely security sensitive so using Rust will probably pay off in the long run. But I don’t think that’s a rule, looks like an exception to me.

Rust FFI is very good. Federico Mena-Quintero's blog has a detailed description of mixing C and Rust code while he rewrote librsvg in Rust. https://people.gnome.org/~federico/blog/librsvg-is-almost-ru...
It’s still foreign. Might be good enough for isolated libraries, but IMO not good enough for the level of integration with OS required by any complex software. Just too much work.

There’s nix::sys::ioctl in Rust stdlib, but there’re also issues with ergonomics, e.g. https://stackoverflow.com/q/51898034/126995 These variable-length structures are used a lot in practice, not just for HID, SPI and USB bulk protocols use similar things. They’re pain to consume from any other language except C and company (C++, obj-c). C# also has very good FFI, but variable-length C structures at API boundaries still require manual marshalling.

There’re third-party bindings for drm, https://github.com/rusty-desktop/libdrm-rs, but apparently that project is not maintained, not sure it works on ARM. It contains more than 3000 lines of code, which will require support. The equivalent C headers, xf86drm.h and xf86drmMode.h, are not small either (800 and 500 lines, respectively), but the important difference is C headers are already supported by Linux kernel so I don’t have to.

I guess this shows my ignorance of low level computing! I used "bare metal" wrong even though Rust can do it.

I wanted to point at a lower abstraction level than a typical corporate application, but higher than bare metal.

Something just above the OS, like any command line.

while it would be nice to have a simple, C like language, with memory safety, but without GC, it may not be possible. Some of the complexity of Rust is to help deal with life of borrow checker land.

There are some middle ground options though, like Zig, which is a nice simple C like language with less undefined behavior and no nulls. so safer, but not offering memory safety.

There is a lot of unexplored wiggle room in the design of borrow checking that might get closer to what you want.

For example, Rust puts &T and &mut T at the forefront, which leads to a slightly alien way of handling aliasing- it's all or nothing. This makes some things feel way harder than they are in C, but helps out the optimizer (every pointer is now restrict/noalias).

A different language could emphasize (the equivalent of) &Cell<T>, which allows shared mutability but restricts certain "shape changing" mutations. Most of those C patterns would feel easy again, with a bit less of Rust's non-safety-essential guarantees.

Cell<T> (1) is not safe to reference across threads, and (2) can only mutate via the equivalent of a memcpy. It can be useful in many ways, but there is a real sense in which &T and &mut T (which would probably be called &uniq T, if Rust devs cared about theoretical cleanness over reusing short keywords!) are truly fundamental.
Point 2 is only a limitation of the current standard library, not of the language-level model. It has even been relaxed recently, so you can go from a &Cell<[T]> to a &[Cell<T>]: https://github.com/rust-lang/rust/pull/61620

The same could be done for struct fields if the type system knew about it, and the whole thing could just use normal syntax.

Sharing between threads still needs &T or &mut T (or an owned value), but that's not usually involved in the painful cases.

It is definitely possible to have Rust's compile time lifetime analysis while having a less complicated language that mostly deals memory automatically: https://aardappel.github.io/lobster/memory_management.html
What are the concepts you have trouble with?
I can’t help but find it a bit like an academic project; a proof of concept. I suppose the complexity of it is warranted to help both you and the compiler from acknowledging unsafe code and get better error messages, so maybe the question is not whether Rust needs to be like this, but whether I as a developer need Rust.