Let's try to put forward arguments against Rust

I recently read an article criticizing Rust. Although it had a lot of the right things, I didnโ€™t like it - too much there is very controversial. Overall, I can't recommend reading any article criticizing Rust at all. This is not good, because it is important to discuss shortcomings, and defamation of low-quality and inept criticism, unfortunately, makes really good arguments ignore.



So, I'll try to argue against Rust.



Not all programming is systematic



Rust is a systems programming language. It provides precise control over data composition and code execution behavior at runtime for maximum performance and flexibility. Unlike other systems programming languages, it also provides memory safety - buggy programs terminate in a well-defined manner, preventing (potentially dangerous) undefined behavior.



In most cases, however, absolute performance or control over hardware resources is not required. For these situations, modern managed languages โ€‹โ€‹such as Kotlin or Go provide decent speed, enviable performance, and memory safety through the use of a dynamically memory managed garbage collector.



Complexity



Programmer time is expensive, and in the case of Rust, you have to spend a lot of time learning the language itself. The community has worked hard to create high quality teaching materials, but the language is very large. Even if it is profitable for you to rewrite the project in Rust, learning the language itself can be too expensive.



The price for improved control is the curse of choice:



struct Foo     { bar: Bar         }
struct Foo<'a> { bar: &'a Bar     }
struct Foo<'a> { bar: &'a mut Bar }
struct Foo     { bar: Box<Bar>    }
struct Foo     { bar: Rc<Bar>     }
struct Foo     { bar: Arc<Bar>    }


In Kotlin, you write a class Foo(val bar: Bar)and start solving a problem. In Rust, you have to make choices, sometimes important ones, with a special syntax.



All this complexity for a reason - we do not know how to create a simpler and more memory-safe low-level language. But not every task needs a low-level language.



See also the presentation Why C ++ Keeps afloat when the Vaza Sank .



Compilation time



Compile time is a universal factor. If a program in some language is slow to run, but that language allows fast compilation, then the programmer will have more time to optimize to speed up the program launch!



In the generics dilemma, Rust deliberately chose slow compilers. This makes some sense (runtime is really speeding up), but you'll have to fight hard for reasonable build times on larger projects.



rustcimplements probably the most advanced incremental compilation algorithm in production compilers, but it's a bit like fighting the language's built-in compilation model.



Unlike C ++, Rust assembly is not parallelized to the limit, the number of parallel processes is limited by the length of the critical path in the dependency graph. The difference will be noticeable if you have more than 40 cores to compile.



In Rust, there are also no analogues for the pimpl idiom , so changing the crate requires recompiling (and not just linking) all its inverse dependencies.



Maturity



Five years is definitely a short time, so Rust is a young language. While the future looks bright, it is more likely that in ten years time we will be programming in C rather than Rust (see the Lindy effect ). If you have been writing software for decades, then you should seriously consider the risks of choosing new technologies (although choosing Java over Cobol for banking software in the 90s turned out to be the right choice in retrospect).



There is only one complete implementation of Rust, the rustc compiler . Most advanced alternative mrustc implementationpurposefully skips many static security checks. Currently rustc only supports one production-ready backend, LLVM. Consequently, support for processor architectures is narrower here than C, which has a GCC implementation, as well as support for a number of proprietary vendor-specific compilers.



Finally, Rust has no official specification. The current specification is incomplete and does not document some minor implementation details.



Alternatives



Besides Rust, there are other languages โ€‹โ€‹for systems programming, including C, C ++, and Ada.



Modern C ++ provides tools and guidelines to improve security. There's even a Rust-style object lifetime safety proposal ! Unlike Rust, using these tools does not guarantee that there are no memory safety issues. But if you already support a large amount of C ++ code, it makes sense to check, perhaps following the recommendations and using sanitizers will help in solving security problems. This is difficult, but clearly easier than rewriting all the code in another language!



If you are using C, you can apply formal methods to proveno undefined behavior, or just thoroughly test everything .



Ada is memory safe unless using dynamic memory (never call free).



Rust is an interesting cost-to-security language, but far from the only one!



Set of tools



Rust's tools are not perfect. The basic toolkit, compiler and build system ( cargo ) are often referred to as best in class.



But, for example, some runtime-related tools (primarily for heap profiling) are simply missing - it's hard to think about runtime if the tool just isn't there! In addition, IDE support also falls far short of Java's reliability level. Automated complex refactoring of a program with millions of lines is simply impossible in Rust.



Integration



Whatever Rust promises, today's systems programming world speaks C and C ++. Rust does not intentionally try to imitate these languages โ€‹โ€‹โ€” it does not use C ++ or C ABI-style classes.



This means that bridges need to be built between the worlds. Integration will not be seamless, insecure, not always cost-effective, and requires synchronization between languages. While integration works in places and the toolkit is converging, there are occasional obstacles along the way due to the overall complexity.



One particular issue is that Cargo's overconfident worldview (great for pure Rust projects) can make it difficult to integrate with larger build systems.



Performance



"Using LLVM" is not a one-size-fits-all solution to all performance problems. Although I donโ€™t know of benchmarks comparing the performance of C ++ and Rust in general, itโ€™s not hard to think of tasks where Rust is inferior to C ++.



Probably the biggest problem is that Rust's move semantics are value-based ( memcpyat the machine code level). On the other hand, C ++ semantics uses special references from which data can be taken (pointers at the machine code level). In theory, the compiler should see the chain of copies, in practice this is often not the case: # 57077 . A related issue is the lack of allocation of new data - Rust sometimes needs to copy bytes to / from the stack, while C ++ can create an object in place.



Funnily enough, the default Rust ABI (which sacrificed stability for efficiency) sometimes performs worse than C: # 26494 .



Finally, while in theory Rust code should be more efficient because of the much richer information about aliases, enabling alias related optimizations causes LLVM errors and incorrect compilation: # 54878 .



But, again, these are rare examples, sometimes the comparison is in the other direction. For example, in Boxat Rust no performance problems, which have in std::unique_ptr.



A potentially bigger problem is that Rust, with its generics definitions, is less expressive than C ++. So some formulaic tricks C ++ for high performance cannot be expressed in Rust with good syntax.



Unsafe value



Perhaps the idea is unsafeeven more important to Rust than ownership and borrowing. By separating all dangerous operations into blocks unsafeand functions and insisting on providing them with a higher-level secure interface, it is possible to create a system that simultaneously:



  1. reliable (unchecked unsafecode cannot cause undefined behavior),

  2. modular (different unsafe blocks can be tested separately).


It is quite obvious that this is the case: fuzzing Rust code finds panic, but not buffer overflows.



But the theoretical prospects are not so bright.



First , there is no definition of a Rust memory model, so it is impossible to formally check whether a given unsafe block is valid or not. There is an unofficial definition of "things rustc does or can rely on" and work is underway on a runtime verifier , but the actual model is not clear. Thus, somewhere, there may be some unsafe code that works fine today, but will be declared invalid tomorrow and break in a new compiler optimization in a year.



Secondly, it is believed that unsafe blocks are not actually modular. Strong enough unsafe blocks can, in fact, extend the language. These two extensions do nothing wrong in isolation from each other, but lead to undefined behavior when used at the same time: see Observable Equivalence and Unsafe Code.



Finally, there are obvious errors in the compiler .



Here are some topics that I have deliberately omitted:



  • Economics ("harder to find Rust programmers") - I think the "Maturity" section captures the essence of this question, which is not limited to the chicken and egg problem.

  • Dependencies ("stdlib is too small / too many dependencies everywhere") - given how good Cargo and the relevant parts of the language are, I personally don't see this as a problem.

  • Dynamic linking ("Rust should have a stable ABI") - I don't think that's a strong argument. Monomorphization is fundamentally incompatible with dynamic linking, and if you really need to, then there is a C ABI. I really think that things can be improved here, but it is unlikely that we are talking about specific changes in Rust .


Discussion topic in / r / rust .



All Articles