After a week with Rust

I'm experienced C++ programmer who have started to use more and more Rust. If you see my previous topics you will see where I struggled with Rust.

Regarding the above, the reason I think is Rc and Arc makes the contained object immutable and it is just easier to declare a variable mutable. C++ equivalent would be using unique pointer and for the same reason programmers don't use it as it makes the reasoning and code changes difficult. Defaulting to shared pointer C++ is the easier way, hence I believe, it has become popular.

But shared pointer in C++ or Rc/Arc in Rust shouldn't be used where it doesn't have to be. Shared pointer has performance cost, same with Arc. Rc is better in performance than shared pointer or Arc but still it doesn't have to be used where it shouldn't have to be.

People struggle with Rust because they can't get away with sloppy practices (compiler enforces better code). But once the code is compiled, programmer knows, chances of having memory bugs are low. For more information see Microsoft Security Response Center blogs and why they started to look into Rust. 70% of the serious security bugs in C/C++ software comes from overlooked memory safety issues and these are one of the top programmers in the world, if they are having difficult time then what it tells about rest of the programmers?

Chances are, there will be memory safety bug if somebody is trying to mutate data across threads without it. If a coder thinks their logic is correct then they can use unsafe and get away with mutation without using Arc<Mutex<T>>.

I meant, take some good C++ code and try to translate it directly into Rust. Everything Rust rejects (and there will be a lot) is something that good C++ programmers are used to doing that they'll have to learn alternatives to in order to make progress in Rust.

2 Likes

I can relate a lot to this. I dived into Rust on a deep end - trying to encapsulate into a common type an SSH connection (using bindings to libssh that have interesting ownership properties) with telnet connection (basically a thin wrapper around TCP).

I made my way with some help from kind people on this and basically tweaking the code until it compiled :slight_smile:

Since then that particular project grew to about 40KLOC, has powered the operation of the network in a rather well known event attended by about 15000 people, so it all somewhat worked :slight_smile:

It’s not a particularly performance-sensitive project (being basically a network management system), but from all the past experience my takeaway is:

  1. When you pass parameters, pass them as reference.

  2. When you return results, return them as owned types.

  3. In struct definitions, use owned types.

  4. derive Debug and Clone traits generously and use {:#?} format with &object (passing the reference!) when printing debug info.

This will largely keep the ownership issues away (or so is my experience), and make debug experience quite pleasant.

Of course, as I said this probably will cost performance, so it isn’t a silver bullet - but this will give you a more comfortable start.

After a while the smaller corner cases will force you into developing a more intuitive sense of ownership and lifetimes and you can move on to fancier types.

I myself only relatively recently (less than a year) started to more aggressively explore traits and references within the data structures. Good luck and have fun! :slight_smile:

1 Like

I think you're tapping on a big "market": some kind of "learn rust the hard way coming from c++", where each chapter is a common c++ pattern and how you'd do it in rust. And I would read it to line it for opportunities to improve the diagnostics so people don't have to read it.

2 Likes

Sorry, yes, I was going by what you actually wrote, not perhaps what you meant:

...consider this exercise: Take a C++ code base you're familiar with, transliterate it into Rust without raw pointers, and see what Rust doesn't like. ...

In the C++ code I've worked on, back pointers are ubiquitous, lifetimes are justified based on high-level application invariants,

All that talk of raw pointers and ill specified object lifetimes does not imply good C++ code to me.

It's a trainwreck. And it's a trainwreck made of everyday practices that the C++ programmers have to find an alternative to if they want to work in Rust.

Do doubt it would be. My feeling is that trying to translate that train wreck of spaghetti pointer code directly to Rust with a liberal sprinkling of 'unsafe' is not the the thing to.

I personally think that Rust calling &uniq references mutable references with the &mut is a big "lie" which ends up biting the programmer back as soon as they start using more subtle stuff:

  • For instance, most people are astonished that the following program fails to compile:

    let mut x = 0;
    let mut inc_x = || { x += 1 };
    inc_x();
    dbg!(x);
    inc_x();
    dbg!(x);
    

Rust has multiple unique paradigms that don't even exist in other languages, such as lifetimes and compile-time-tracked "exclusive access". But instead of endorsing them from the beginning, as @mbrubeck's Rust: a unique perspective does, the Rust book tries to show a language that is "like other languages, but with (magical) compile-time checks". When the truth is that Rust's strength lies in non-unsafe Rust being less expressive than languages like C or C++.


I think that Rust should start with that statement: "Welcome to a language that by being less expressive forces you to use contructs that are guaranteed at compile-time to be sound. But don't worry, after some time you will get used to the coding patterns that are allowed, and will then almost not notice the hindered expressiveness, only the enhanced zero-cost safety that will let you hack without fear".

  • It doesn't sound bad imho, and is at least honest w.r.t. the struggles that someone refusing to shift their way of coding / mental coding patterns may encounter.
12 Likes

Ah, thanks for linking the unique perspective post. I was looking for it but couldn't find it when I was writing this post earlier today.

1 Like

The pun is what helps me remember it :smile:

2 Likes

As some have observed here, I think it's probably true that people who come from C or C++ have difficulty with Rust because Rust forces a safe programming modality on them that they haven't experienced in the older languages. Though I would argue that some of what the Rust compiler prevents you from doing is actually safe. For example, I ported a single-threaded program from C to Rust that talks to Sqlite and Rust simply would not allow me to do prepared-statement caching (in 'safe' code) in a way that was simple and safe in C. The reason is that there is no way in Rust to say "this code is always going to be single-threaded, so stop bothering me with worries about multi-threadedness; it isn't a problem".

But the migrants from C and C++ are only a portion of those who have difficulty learning Rust. Another group are those who come from garbage-collected languages, whether statically- or dynamically-typed, and in Rust they find a language where they have a lot more responsibility for memory management than they did in the language from which they came. These are the automatic-transmission people finding themselves in a car with a dual-clutch manual (C and C++ would be analogous to traditional manual transmissions). They have more to learn and more to do to drive the car. And I think the the gulf between, say, Go or even Haskell (in which I've written a lot of code and in which I'm quite a bit more productive than in Rust, despite the famously fussy GHC compiler) is greater than the car example.

So just as I would not recommend a manual transmission or dual-clutch manual to someone who is going to do a lot of city driving, I would not recommend Rust to someone about to write an application that would be fine in Go. It's a matter of using the right tool for the job. Rust is advertised as a "systems" language and I think that is entirely appropriate. And, as I said in prior post, if I had to recommend a language to someone about to start on a real "systems" project and they reflexively reached for C or C++, I would absolutely try to point them (pun intended) toward Rust.

2 Likes

I look at this idea of "expressive" from the other end of the telescope....

I could write all my code in binary or hexadecimal. I have actually done that back in the day. When I do that I can do anything I like. But my binary or hexadecimal does allow me to express concepts such as conditionals, loops, data types functions, structures, arrays, closures, classes, lists, iterators, nothing. In fact it does not allow me to express the difference between an instruction and data! I have to take care of everything that myself.

Stepping up a level I could write all my code in assembler. Now at least expressing instructions is possible and they are distinguished from data. I can do anything I like but my assembler does not allow me to express concepts such as loops, data types, arrays, functions, structures, closures, classes, lists, nothing much we expect from high level languages. If I want to use such concepts I have to take care of that myself.

Step up another level I could write all my code in C. Great, now I can express the notion of functions, structures, conditionals, loops, pointers and so on. Even recursion is a thing. Wow! But if I want multiple instances of things, as in OOP, I need to take care of that myself.

Step up again and we have C++. Now I can easily express that multiple instance thing, classes, inheritance etc.

Or we could go up levels in a different direction, Haskell for example.

What about Rust?

Rather than saying "Rust is less expressive" I could make the case that it continues up the abstraction ladder by being more expressive. This time it allows the expression of data ownership and allowed aliasing.

As far as I know Rust is unique in that high level expressive power.

4 Likes

That is because C++ doesn't check for multiple mutable borrows.
Above code will work correctly in single threaded application, but Rust design team probably looked at this issue and may have kept this constraint for a reason.

In my humble opinion, it helps write better code and it will be easier to convert to concurrent/parallel code.

The borrow checker isn't that much of a problem, it is the limitation of the references that is the Achilles heel of Rust. Just allowing one mutable reference is very limiting and it seems that Microsoft has acknowledged this with Verona, we will see what type of solution they will come up with.

Instead you have to use things like storing indexes into arrays because it is "safe". Then again that index can be wrong and you have strangely behaving program anyway. Just saying that a language is safe isn't a guarantee it is bug free and people who says "if it compiles it is correct" are crazy.

I just think that Rust is a too limiting language for me and I wouldn't consider it for anything right now. There are things that I like with Rust that I think it got right which are, Utf8 is first class citizen, properly checked utf8 sequences checked as standard, extensive use of optional type, enum type is useful, iterators are powerful but almost too many of them. These are good things but it doesn't weigh up against the drawbacks.

Static analysis of lifetimes is an interesting paradigm in compiler design but I think Rust is just a step on the way and I'm not sure they got this right from the beginning.

3 Likes

That is interesting. I figured they will probably make their own Rust like language:

If anyone interested: Microsoft: We're creating a new Rust-like programming language for secure coding

I'm new to Rust and all this. So could you explain what you mean by that?

To my neophyte mind as soon as you have more than one reference and one of them is mutable you have a potential problem. Do you have a case where that is not so?

Just now I have been converting a C++ program to Rust, as a little C/C++ vs Rust performance race Tatami Pi Fun. - Page 10 - Raspberry Pi Forums. That has proved problematic due to the borrow checker. On the other hand just only hours ago it was discovered the original C++ code has a race condition that causes it to fail sometimes. This is not even a big program.

I liken the situation to the idea that years ago it was declared that goto is harmful (like "unsafe" to me). Sure enough people complained like hell, "Nooo, I can't express myself without goto", "Performance will suck without goto"

Sure you can write code that works reliably with 'goto', sure there may be a performance benefit some times. In general though 'goto' causes more trouble than it is worth Goto Fail, Heartbleed, and Unit Testing Culture.

Similarly people complain about the borrow checker "Nooo, I can't express myself with that, I know my code is correct"

Yeah, right...

4 Likes

Microsoft is already using a significant amount of Rust, and all signs point to more. They also sponsor the project’s CI.

1 Like

That doesn't mean they won't build a programming language competing with Rust. Following is the interesting part in the article:

"The ownership model in Verona is based on groups of objects, not like in Rust where it's based on a single object. In C++ you get pointers and it's based on objects and it's pretty much per object. But that isn't how I think about data and grammar. I think about a data structure as a collection of objects. And that collection of objects as a lifetime.

"So by taking ownership at the level of ownership of objects, then we get much closer to the level of abstraction that people are using and it gives us the ability to build data structures without going outside of safety."

Sure. Microsoft Research is different than “Microsoft”, and not all of their languages even make it out of the experimentation phase. That’s what they do!

Microsoft guy:

I think about a data structure as a collection of objects. And that collection of objects as a lifetime.

Does he mean a collection of objects like a Linux or Windows kernel? Like libc and so on...

"So by taking ownership at the level of ownership of objects, then we get much closer to the level of abstraction that people are using and it gives us the ability to build data structures without going outside of safety."

I read this a couple of days ago. Can someone who knows about these things interpret what it means?

My naive mind reads it as "We don't want to bother with all that detailed anti-aliasing stuff that Rust does. We want to write thousands of lines of code to implement whatever data structures and collections of objects we like with out all that hassle. We will put the safety checks only at the API level of that collection to make user code safe. Trust us what is inside the black box will be safe"

All of which sounds like my Rust programs that, whilst being "safe" in themselves, make use of millions of lines of unsafe code in libc, pthreads, the Linux kernel etc. Or similar unsafe code when running on Windows.

Surely the idea is to move such safety down the stack as much as possible.

3 Likes

I think that is not surprising. Microsoft wants a system language replacement for C and C++ code, which is Rust, and they want an easier language which can maybe be part of the .NET family for general application development like C#, F# and VBA. They didn't try to replace C++ with C#, just tried to make Windows app development more accessible and safer. Same here, IMO.