Replacing of C with Rust has been a great success

Replacing of C with Rust has been a great success

kornelski

Rust has everything I wanted C to do better, and it achieved it without any of the accidental complexity that made me avoid C++.

If I didn't try Rust, I'd probably scare myself away from it by drawing a false equivalency between Rust and C++. For example, both languages have two string types, and that looks bad. In C++ this is an unfortunate complication due to legacy reasons. In Rust, this is a deliberate design, which is a logical consequence of ownership: one string type owns its memory, which enables automatic memory management without a runtime GC, and the other string type is a thin immutable view that makes passing of strings and working with substrings safe and efficient. C has the same distinction (you can't pass any string to free()), but it's informal and the C compiler can't help with it.

BTW, this is also notably different from Go, where copying of strings and lifetime of substrings are implementation details largely hidden by the GC. Similarly, Go slices obscure ownership, so when you append to a slice there's no guarantee that it won't overwrite data that is shared with some other slice.

Evolution of the C language is essentially dead. C99 is 20 years old, and it's still somewhat new and unproven. Nobody even cares what happened to C later. Microsoft has decided not to maintain its C compiler beyond the minimum required by C++, so even if the C spec improved anything substantial, It'd be doomed as "non-portable".

But the C language is not done. There are many things that C could fix to make it safer and simpler to use. For example, signed overflow is an undefined behavior. The UB is easier to trigger than it seems, because even arithmetic on unsigned types can become signed due to integer promotion rules. Overflows in calculations of buffer sizes are a known way to exploit software (which has happened, and has been exploited at scale). If that wasn't bad enough, checking for overflow can backfire terribly, because the naive way is an UB itself and is "proving" to the compiler that the overflow "can't happen". So given it's a common problem, easy to get wrong, with serious consequences, very hard to detect by static analysis, what does C do to fix it? Thoughts and prayers.

OTOH in Rust integer overflow is a defined behavior. In debug builds Rust checks all arithmetic for overflows (and can also do it in optimized builds, if you like such trade-off), and there are checked, saturating, and wrapping methods to handle overflow explicitly where needed.

  • C: 0 problems fixed per year. Despite C programmers making the same mistakes over and over again, C18 did literally nothing about it.
  • Rust: 15 problems fixed per year. The Rust team continually learns from how Rust is used, and works to help programmers be successful with the language.

The aforementioned overflow checking was added after Rust 1.0.

Rust modules started with a rather simple, logically self-consistent definition, but they felt complicated, because they didn't match users' intuition. After a lot of user input and careful deliberation, they were extended to behave more like users expect.

The original borrow checking rules based strictly on scopes were (superficially) simple to define and explain, but turned out to be hard to use in real-world programs. A lot of effort and implementation complexity was spent on making them more flexible.

Match statements demanded programmers to be very precise about references, but it felt too pedantic and was unnecessary, so the compiler was extended to figure out the details itself, etc. etc.


If you only counted headers in release notes, you could assume the language is getting bloated. But Rust today is a simpler-to-use language than it was in 2015.

Rust is so portable, it works even on Windows

In the C world, Windows causes me a lot grief. I tried to ignore it, but there's a lot of Windows users. They've struggled with MinGW/Cygwin and kept complaining that my library doesn't link with their Visual Studio projects. Eventually, I gave in, and spent time working around stupid Windows issues. Now I have a bunch of #ifdefs to maintain, some dumbed-down fragile syntax for MSVC, hacks in the build system, and my project still feels second-class on Windows.

But I support Windows in all my Rust projects, because Rust and Cargo have full support even for this "exotic" platform. I can even use Unicode filenames without touching the wide char nonsense. I've made cargo-deb (which builds Debian packages from Rust projects) work natively on Windows, because the amount of effort required was small enough to make that joke.

Rust doesn't have spec lawyering

C looks simple, feels simple, and yet, there's a 550-page long spec. If I write something that causes nasal demons, it's my fault for not knowing the spec. StackOverflow will even gleefully quote the page that says my code is bad and I should feel bad.

OTOH when Rust programmers shoot themselves in the foot, it's treated as a bug in Rust. The language is expected to work in a reasonable and predictable way, even in edge cases. The end result is that the Rust compiler is a pleasure to work with, and my Rust programs are more robust than my C programs.

Rust has no fragmentation

The upside to Rust being relatively new is that I don't have to worry about someone's broken compiler from 1989. If something is documented to work, it works. If a new compiler flag is added, I can rely on it within a couple months.

In C I could use make, gnu make, cmake, gyp, autotools, bazel, ninja, meson, and lots more. The problem is, C programmers have conflicting opinions on which of these is the obvious right choice, and which tools are total garbage they'll never touch.

In Rust I can use Cargo. It's always there, and I won't get funny looks for using it.

Rust works nicely with the C ABI

Because Rust uses LLVM and doesn't have a runtime, it's usable in basically every environment where C is. I can write static libraries. I can write plugins for C programs and "native" modules for other programming languages.

Cargo is awesome

In C building things is endless tweaking of build systems, detection of paths and flags, and cleaning up of temporary files. From C's perspective dependencies are a pain, a liability and best avoided entirely.

But Cargo has none of it. If I want a Rust library, I add its name to the list of dependencies, and boom, it's there. It compiles. It works. Cross platform.

This even works nicely with popular C dependencies, because Cargo has a mini ecosystem of build-time packages that work around platform differences and snowflakyness of C libraries. If I'm forced to use OpenSSL, on macOS or Windows, I don't mess with tarballs and include paths. I add openssl-sys to Cargo and automatically get someone else's battle-tested build script that has already figured this all out. It will even check Windows registry to find where Visual Studio puts the necessary headers.

I refuse to use anything less nice than this.

Concurrency is finally robust

In languages with shared mutable state, including Go, concurrency can mess things up in ways that are painfully hard to debug. In C, whenever I tried to do non-trivial parallel computation, I ended up regretting it.

To give you an example how fearless concurrency looks in Rust: Rust has a built-in unit testing framework that runs all unit tests in parallel. It doesn't even ask — it's parallel by default. That's because it can safely assume that every Rust program is entirely thread safe, even if the programmer didn't mean to write a thread-safe program. It's that good.

More than just safety

Features that make C safe enough, like R^X, ASLR, canaries, guard pages, sandboxing, tagged pointers, etc. are focused on crashing faster, to stop the program before it can be exploited. Thanks to shared OS and LLVM infrastructure Rust has the same protections as defense in depth, but it goes beyond that. Non-nullable types, mandatory error handling, and loops without room for off-by-one errors prevent entire classes of bugs from being written in the first place. It makes programs not just safer, but more reliable.

When I start a new project I have an ambition to write everything perfectly, but that feeling doesn't survive through all my late-night coding sessions, quick fixes, deadlines, and overly optimistic "don't worry, that won't happen". I'm very grateful that Rust keeps me in check and doesn't let me cut corners too much. After switching to Rust the nature of my bugs has changed considerably: now they're mostly domain-specific, such as unimplemented features, not behaving according to the spec, and at worst gracefully reporting an error when things should have worked. I don't deal with segfaults any more. I haven't even needed lldb and Valgrind for cases other than integrating C into Rust programs.


Overall, the switch to Rust has made my programming easier and more productive. Memory leaks are no longer a problem. Dependencies are no longer a problem. Windows support is no longer a problem. Cleanup on errors is no longer a problem. Data races are no longer a problem. Null pointers are no longer a problem. Uninitialized variables are no longer a problem. Calculating of buffer sizes is not a thing. Headers and preprocessor don't exist. All of this mundane busywork of C is just gone. This feels like cheating, because the years of being a C programmer have taught me that these are unavoidable hard problems that real programmers have to deal with it every day, and that's the price to pay for a bare-metal language. And it turned out to be false — it was just C making basic things unnecessarily hard, because it didn't know any better.

Report Page