So I'll instead just give reasons why people might prefer to program in Rust:
- Memory management / safety (no GC, variable lifetimes are tracked by the compiler, no dangling, no null pointer ...)
- Ergonomics (built-in unit tests, feature-full std lib, unicode strings, modern dependency manager, more formalized macros ...)
- Functional-lite (you must explicitly discard unused enum variants, the "null" value is an enum variant ...)
Long story short: Rust makes it really easy to write sound software. I think you see that on crates.io where Rust packages are hosted. Lots of packages quickly reach maintenance mode and receive very few updates, but remain extremely useful. Once you've written (compiled) something in Rust, you feel much more confident that is is free of latent runtime bugs.
Without commenting on whether or not this is necessary or good for the Linux kernel, I'll say that there are probably many devs out there that feel like working other non GC languages feels like pulling teeth after having worked with Rust. And that might be part of the pressure.
Though dealing with rust dependencies by hand is usually nicer than dealing with C dependencies by hand because the build process is more standardized.
Dangling pointers are not a thing.
No double free.
Forces you to think up front about what are the possible state something could be in, and so you won’t get runtime or weird UB.
lastly, to be more pedantic for everyone who is at Linus-levels, it’s like all the best static analysis and warnings/errors all rolled up natively into the language. Please don’t bombard me with “but I can just run clang’s X and get the same thing”. No you can’t, and if you did this forces you to run it regardless.
Yes, we can.
Then you still have to run extensive fuzzing and testing to trigger different paths and whatnot.
Or you could just use Rust.
Type inference
Language support for tagged unions (which they call enums for some reason), which among other things results in error handling which is "out of channel" instead of magic return values.
Speaking out error handling - syntax for easily forwarding errors to the calling function.
Generics, and therefore
- Libraries of collections with a rich collection of methods implemented for you.
- Library support for lazy iteration
- ...
Method (postfix call) syntax allowing for readable chaining
Hygenic macros
Compiler supported test harnesses
Compiler supported linting (that is really good)
Compiler supported automatic formatting (that is really good)
Compiler supported vtables
(pointer, length) strings
Proper namespaces
And so on and so forth
Probably because the simplest case of Rust enums (without any data other than the tag itself) can act similar to a C enum, to the point that you can "#[repr(C)]" it and use as FFI for the equivalent C enum. One understated advantage of Rust for C programmers (which is on topic given that the Linux kernel is written mostly in C) is that its syntax and keywords have many similarities with C, starting with the use of braces for blocks and semicolons to end statements.
> and use as FFI for the equivalent C enum.
Only if you're willing to accept undefined behavior if the foreign code passes you an unknown variant. For actual ffi work I've ended up using a macro that makes a "#[repr(C)] struct EnumName(u32)" with a bunch of constants for the values.
Isn't that what `#[non_exhaustive]` is for?
If there are any values that don't fit in a u32, I would need to use a different int type. C enums automatically use bigger int types if they don't fit in the smaller ones.
If I was on an architecture where int was smaller than 32 bits it would also be incorrect (except by coincidence that 32 bits was the smallest type that could hold all the values). I could "solve" that by using libc::c_int instead, but that only sort of solves the issue because if libc::c_int is 8 bits, and the enum has a value 257, it's just not correct anymore.
I suppose it's possible that some abi treats a struct containing an int differently from an int, I doubt it though. If that's the case, #[repr(transparent)] instead of #[repr(C)] would be slightly more correct.
In some ABIs this is not true, a C enum is a char if the constants fit. Your code that assumes enums are stored and passed around as 32-bit integers is not entirely portable.
Ultimately, at least at the time, there was no other option that had better compatibility. Now I suppose you could use MaybeUninit<#[repr(C)] enum> or something. Realistically I don't have much confidence in rustc properly matching enum sizes with C implementations that don't follow the typical behaviour here, so I doubt that would be worth it.
I guess the linux kernel is generic enough that it might end up on some such platforms, I guess in that context I'd just bite the bullet and accept undefined behavior if passed an unexpected enum variant, it's not like that should happen in a coherent system like the linux kernel (vs my use case which was wrapping some library that definitely might chose to add enum variants).
For example your Rust function that always returns 42 should probably be:
const fn fortytwo() -> i32 {
42
}
But C programmers might try: const fn fortytwo() -> i32 {
return 42;
}
And that works fine, the compiler sees that you wanted to return 42 here, and, although the value at the tail of the function is an empty tuple and thus incompatible with your function signature you never reach that tail, so no problem.2. Ergonomic use of functional idioms like sum types (Rust's enums)
3. Memory safety without GC overhead - Windows, Linux, curl, over and over again 40% of vulnerabilities in larger C projects are found to be preventable if there was a memory safe language
The ownership/lifetime system is basically novel to Rust (well, it's based on Cyclone, which would probably have been an interesting choice for the kernel too because it's based on C, but Cyclone is no longer actively developed), and it allows you to get the performance of manual memory management (no refcounting required) without either leaking memory or hitting use-after-free bugs.
Rust's ownership system also tracks shared/immutable references and unique/mutable references at compile time, avoiding a whole bunch of setups for creating race conditions or otherwise corrupting data.
> Rust's ownership system also tracks shared/immutable references
But shared/immutable vs. unique/mutable references aren't the same thing, they're something more interesting. Rust has three types of ways you might be able to get at a variable: you could own the variable, you could have a shared/immutable reference to it, or you could have a unique/mutable reference to it. The Nim proposal only distinguishes the ownership case (responsible from freeing the data when it's done - basically RAII or __attribute__((cleanup)) or autorelease) from the unowned case.
Having shared/immutable references gives you a neat feature for achieving correctness, not just memory safety (and I strongly disagree that the idea of "memory safety" in that post really counts, incidentally, because by that definition Heartbleed wasn't a memory-safety bug, but for the sake of argument let's say it does). The compiler prevents you from having more than one mutable reference at a time. It also prevents you from writing data you own while someone holds a reference to it, or even reading data while someone holds a mutable reference. So if you're writing code where you pass a reference to some other routine, and that function ends up holding onto it for future use, you won't be able to modify it. That prevents logical bugs where code doesn't realize that data has changed or conversely doesn't realize that a change hasn't taken effect.
Again, this is not solely about memory safety. This is about easily writing correct code, of which memory safety is one aspect.
You can think of this as basically type-level reader/writer locks. At any given time you can have multiple readers and no writer, or exactly one writer. While runtime memory safety is one thing that reader/writer locks can do (e.g., preventing you from calling realloc() while other people still have a pointer that they intend to read), they're used for a lot more than that.
Furthermore, Rust's approach requires no runtime overhead and preserves correct behavior. Nim's approach requires refcounting, and the post argues that refcounting could be disabled in a release build if you're confident your code is correct.
Nim had the concept of borrowing from very early on. It's not a bolt-on, it's part of the design philosophy. But it's had consistency issues. The move semantics in newer Nim improve on it.
I would say one of the major things Rust offers over Nim at the moment is the implementation quality of the Rust compiler, standard library and type systems is generally of higher quality and more consistent. Things are thought-through more completely in Rust's design, and tested more against real world problems.
The multi-threading in Rust is also of much higher quality. It's just buggy in Nim.
Nim however has a much cleaner syntax, and the quality issues could change if the community really wanted that (I don't think it does).
They both have slow compile times for large programs.
But there are some significant improvements WRT memory management/indirection.