I think Rust is harder to learn, but once you grok it, I don't think it's harder to use, or at least to use correctly. It's hard to write correct C because the standard tooling doesn't give you much help beyond `-Wall`. Rust's normal error messages are delightfully helpful. For example, I just wrote some bad code and got:
--> src/main.rs:45:34
|
45 | actions.append(&mut func(opt.selected));
| ---- ^^^^^^^^^^^^ expected `&str`, found `String`
| |
| arguments to this function are incorrect
|
help: consider borrowing here
|
45 | actions.append(&mut func(&opt.selected));
|
I even had to cheat a little to get that far, because my editor used rust-analyzer to flag the error before I had the chance to build the code.
Also, I highly recommend getting into the habit of running `cargo clippy` regularly. It's a wonderful tool for catching non-idiomatic code. I learned a lot from its suggestions on how I could improve my work.
> I think Rust is harder to learn, but once you grok it, I don't think it's harder to use, or at least to use correctly. It's hard to write correct C because the standard tooling doesn't give you much help beyond `-Wall`.
When I say Rust is harder to use (even after learning it decently well), what I mean is that it's still easier to write a pile of C code and get it to compile than it is to write a pile of Rust code and get it to compile.
The important difference is that the easier-written C code will have a bunch of bugs in it than the Rust code will. I think that's what I mean when I say Rust is harder to use, but I'm more productive in it: I have to do so much less debugging when writing Rust, and writing and debugging C code is more difficult and takes up more time than writing the Rust code (and doing whatever less debugging is necessary there).
> Also, I highly recommend getting into the habit of running `cargo clippy` regularly. It's a wonderful tool for catching non-idiomatic code.
That's a great tip, and I usually forget to do so. On a couple of my personal projects, I have a CI step that fails the build if there are any clippy messages, but I don't use it for most of my personal projects. I do have a `cargo fmt --check` in my pre-commit hooks, but I should add clippy to that as well.
If you're using VS Code then you can add `"rust-analyzer.check.command": "clippy"` to your `settings.json`. I assume there's a similar setting for rust-analyzer in other editors.
Package managers are for running other people's code, I would not expect the same of static analysis tools, especially since they are of use while auditing other people's code before building/running it.
Cargo's threat model here is identical to that of rust analyser. If you trust your dependency tree sufficiently to run `cargo build`, then you trust it sufficiently to run rust analyser.
Rust analyser executes those exact same build scripts. This is the primary avenue for exploits in both pieces of software, and is called out explicitly in the page you originally linked.
That's a fair distinction. Basically, it's easier to write C that compiles than Rust that compiles, but it's harder to write correct C than correct Rust.
Regarding Clippy, you can also crank it up with `cargo clippy -- -Wclippy::pedantic`. Some of the advice at that level gets a little suspect. Don't just blindly follow it. It offers some nice suggestions though, like:
warning: long literal lacking separators
--> src/main.rs:94:22
|
94 | if num > 1000000000000 {
| ^^^^^^^^^^^^^ help: consider: `1_000_000_000_000`
|
> it's still easier to write a pile of C code and get it to compile than it is to write a pile of Rust code and get it to compile.
As someone who is more familiar with Rust than C: only if you grok the C build system(s). For me, getting C to build at all (esp. if I want to split it up into multiple files or use any kind of external library) is much more difficult than doing the same in Rust.
Rust allows to provide more information about types (generic types, pointer usage) and checks it, while in C you have to rely on doc comments and checking the code manually. Or am I wrong and C allows to specify pointer nullability, pointer ownership and array bounds?
None of those things feature in any problem I deal with on a daily basis, whatever language I use.
So for example today I dealt with a synchronization issue. This turned out to not be a code bug but a human misunderstanding of a protocol specification saga, which was not possible to code into a type system of any sort. The day before was a constraint network specification error. In both cases the code was entirely irrelevant to the problem.
Literally all I deal with are human problems.
My point is Rust doesn't help with these at all, however clever you get. It is no different to C, but C will give you a superset of vulnerabilities on top of that.
Fundamentally Rust solves no problems I have. Because the problems that matter are human ones. We are too obsessed with the microscopic problems of programming languages and type systems and not concentrating on making quality software which is far more than just "Rust makes all my problems go away" because it doesn't. It kills a small class of problems which aren't relevant to a lot of domains.
(incidentally the problems above are implemented in a subset of c++)
> So for example today I dealt with a synchronization issue. This turned out to not be a code bug but a human misunderstanding of a protocol specification saga, which was not possible to code into a type system of any sort.
Maybe not in a reasonable language no, but there are advances in type systems that are making ever larger classes of behaviours encodable into types. For example, algebraic effects (can this function throw, call a remote service etc)
Vulnerabilities are bugs, so the C code will have more bugs than the Rust program.
You might say that the C and Rust code will have the same number of logic errors, but I'm not convinced that's the case either. Sure, if you just directly translate the C to Rust, maybe. But if you rewrite the C program in Rust while making good use of Rust's type system, it's likely you'll have fewer logic errors in the Rust code as well.
Rust has other nice features that will help avoid bugs you might write in a C program, like most Result-returning functions in the stdlib being marked #[must_use], or match expressions being exhaustive, to name a couple things.
> most Result-returning functions in the stdlib being marked #[must_use]
Actually it's a bit cleverer than that, and some people might benefit from knowing this. The Result type itself is marked #[must_use]. If you're writing a Goat library and you are confident that just discarding a Goat is almost always a mistake regardless of the context in which they got a Goat you too should mark your Goat type #[must_use = "this `Goat` should be handled properly according to the Holy Laws of the Amazing Goat God"] and now everybody is required to do that or explicitly opt out even for their own Goat code.
Obviously don't do this for types which you can imagine reasonable people might actually discard, only the ones where every discard is a weird special case.
Types I like in Rust which help you avoid writing errors the compiler itself couldn't possibly catch:
Duration - wait are these timeouts in seconds or milliseconds? It's different on Windows? What does zero mean, forever or instant ?
std::cmp::Ordering - this Doodad is Less than the other one
OwnedFd - it's "just" a file descriptor, in C this would be an integer, except, this is always a file descriptor, it can't be "oops, we didn't open a file" or the count of lines, or anything else, we can't Add these together because that's nonsense, they're not really integers at all.
How can it not be true? One of the primary features of the rust compiler is enforcing memory safety at compile-time. C doesn't have anything like that. There are an entire class of bugs that are impossible to implement in rust.
I have written a lot of C and Rust. The notion that identical projects written in both languages would have identical numbers of (or even severity of) bugs is laughable on its face.
I mean, literally just not being able to deref a NULL pointer by itself is enormous.
You can dereference null pointers in Rust, it's also easy to make null pointers. The thing is I'm not surprised you didn't realise this is possible because you would never actually do this in Rust since it's obviously a bad idea.
let p: *const i32 = std::ptr::null(); // Pointer to a signed 32-bit integer, but it's null
unsafe { some_local = *p }; // Dereferencing the null pointer, bang
If we instead use references they're nicer to work with, don't need unsafe, perform the same, and are never null. So obviously that's what real Rust programmers almost always do.
Of course it’s possible. I am well aware it’s possible. You can also just call into a C library that hands you a null pointer. Further, it’s wholly possible to invalidate every single one of Rust’s safety guarantees if you try hard enough.
In practice I have never encountered a null pointer in Rust. Most Rust programmers have never encountered a null pointer. For most non-FFI practical purposes you can act as if they don’t exist.
> Also, I highly recommend getting into the habit of running `cargo clippy` regularly.
You can also have that hooked up to the editor, just like `cargo check` errors. I find this to be quite useful, because i hace a hard time getting into habits, especially for thing that i'm not forced to do in some way. It's important that those Clippy lints are shown as soft warnings instead of hard errors though, as otherwise they'd be too distracting at times.
> It's hard to write correct C because the standard tooling doesn't give you much help beyond `-Wall`
I won't disagree that correct C is harder to write, but it's not 2005 anymore and standard tooling gives you access to things like asan, msan, ubsan, tsan, clang-tidy...
C-with-sanitizers is miles ahead of what C used to be, but just a couple weeks ago I ran into a dangling pointer bug that ASan doesn't catch. (Sidenote 5 here: https://jacko.io/smart_pointers.html) It seems like one of the big downsides of sanitizers is that they can't instrument other people's code, in this case the C standard library.
* Rust errors can be equally unhelpful. Also, the error you posted is hands down awful. It doesn't tell you what went wrong, and it's excessively naive to rely on compiler to offer a correct fix in all but the most trivial cases. When errors happen, it's a consequence of an impasse, a logical contradiction: two mutually exclusive arguments have been made: a file was assumed to exist, but was also assumed not to exist -- this is what's at the core of the error. The idiotic error that Rust compiler gave you doesn't say what were the assumptions, it just, essentially, tells you "here's the error, deal with it".
* In Rust, you will have to deal with a lot of unnecessary errors. The language is designed to make its users create a host of auxiliary entities: results, options, futures, tasks and so on. Instead of dealing with the "interesting" domain objects, the user of the language is mired in the "intricate interplay" between objects she doesn't care about. This is, in general, a woe of languages with extensive type systems, but in Rust it's a woe on a whole new level. Every program becomes a Sisyphean struggle to wrangle through all those unnecessary objects to finally get to write the actual code. Interestingly though, there's a tendency in a lot of programmers to like solving these useless problems instead of dealing with the objectives of their program (often because those objectives are boring or because programmers don't understand them, or because they have no influence over them).
I don't follow your first point—the compiler is pointing out exactly what the problem is (the argument has the incorrect type) and then telling you what you likely wanted to do (borrow the String). What would you see as a more helpful error message in this case?
The compiler says "expected X, but found Y". I don't know how to interpret this: is the type of the thing underlined with "^^^" X or Y? "Expected" and "found" are just like "up" and "down" in space: they are meaningless if you don't know what the compiler expects (and why should it?).
What it needs to say is something along the lines of "a function f is defined with type X, but is given an argument of type Y": maybe the function should be defined differently, maybe the argument needs to change -- it's up to the programmer to decide.
I dunno, I feel like if you've used a compiler regularly, "expected X, but found Y" is a pretty common idiom/shorthand that people understand. Your wordier version of that feels unnecessary to me.
C is a low level language and deals with things close to the metal. It's probably not fun to write a large business app in barebones C but you having control over low level things makes other things possible and very fast too. Depending on the type of problem you have use the appropriate and favorite language.
Since it's underlining code you wrote, it must be "found" that is highlighted, not "expected". Much like up and down, gravity exists to ground all of us in the same direction.
I'm over here with TTS: Underlining in a terminal rarely translates to audio. It isn't the only consideration that needs to be made, when making things clear.
One thing that could help here is that the compiler is able to offer output in JSON, allowing you to format the messages however you'd like: https://doc.rust-lang.org/rustc/json.html
I'm not aware of an existing tool to produce blind-friendly output, but this would at least be a part of that!
The code tend to be loaded with primitives that express ownership semantics or error handling. Every time something changes (for instance, you want not just read but also modify values referenced by the iterator) you have to change code in many places (you will have to invoke 'as_mut' explicitly even if you're accessing your iterator through mutable ref). This could be attributed (partially) to the lack of function overload. People believe that overload is often abused so it shouldn't be present in the "modern" language. But in languages like C++ overload also helps with const correctness and move semantics. In C++ I don't have to invoke 'as_mut' to modify value referenced by the non-const iterator because dereferencing operator has const and non-const overloads.
Async Rust is on another level of complexity compared to anything I used. The lifetimes are often necessary and everything is warpped into mutliple layers, everything is Arc<Mutex<Box<*>>>.
> Rust errors can be equally unhelpful. Also, the error you posted is hands down awful. It doesn't tell you what went wrong, and it's excessively naive to rely on compiler to offer a correct fix in all but the most trivial cases
What? It tells the user exactly what's wrong
> Every program becomes a Sisyphean struggle to wrangle through all those unnecessary objects to finally get to write the actual code
That is the cost of non-nullable types and correctness. You still have to do the Sisyphean struggle in other programming languages, but without hints from the compiler.
Also, I highly recommend getting into the habit of running `cargo clippy` regularly. It's a wonderful tool for catching non-idiomatic code. I learned a lot from its suggestions on how I could improve my work.