My First Three Months with Rust​

I’ve used C++ professionally in games and simulations for over 10 years, and in the past few years I’ve also used C# to build distributed backend systems. Lately, I’ve been exploring rust.

I’ve used C++ professionally in games and simulations for over 10 years, and in the past few years I’ve also used C# to build distributed backend systems.

Lately, I’ve been exploring rust.

Why Rust?

My interest in Rust grew as I became aware of these three facts:

  1. Rust has been the “most loved” language in the stack overflow developer survey four years in a row.
  2. Rust is a memory-safe language with performance comparable with C++! (It in fact goes through the same optimizer that clang uses, LLVM) (See also latest techempower benchmarks)
  3. Rust carries forward two very important features of C++:  destructors, and const-ness.

My Favorite Feature in C++ is Back!

Memory, locks, file handles, database transactions, TCP sockets: Good software engineering is all about resource management.

In my opinion, the single most important feature of C++ is the destructor. RAII-style programming leverages the destructor to ensure that cleanup code always runs. Further, it's clearly defined when it will run. For many of these resources, the timing is crucially important.

void write_to_db(DatabaseConnection &connection)
{
    Transaction tx = connection.StartTransaction();
    Data d = get_data();
    tx.write_data(d);
    
    // * Transaction has a destructor, so we can't forget 
    //   to clean it up.
    // * Even if get_data() throws an exception, it is still 
    //   guarateed to be cleaned up!
    // * Any code that run after leaving this function can safely
    //   assume the transaction is written
}
C++ destructors are an invaluable tool for writing correct code! They can ensure that transactions are committed, files and sockets are closed, and memory/locks are released.

I'm having a hard time thinking of a recent popular language that isn't garbage collected. I think garbage collection can be a good solution for managing memory in many cases, but I also believe that garbage collection is a targetted solution for memory management that ignores the general problem of resource management.

const++

Rust has the concept of immutability, and when I first looked at the language, I thought that the mut keyword was roughly equivalent to not-being-const in C++. (In Rust, dangerous features are opt-in, rather than opt-out.) But as it turns out, rust takes the concept further.

Many languages (like C++) are not particularly “aware” of threads. Sure, you can call an API that will make the OS spawn a new thread, executing your code. But there is no static analysis that reasons about that thread, and there are no language facilities to add helpful annotation for that analysis. Most languages make no effort to help you write correct multi-threaded code.

use std::thread;
fn main() {
    let mut numbers = vec![17, 42, 5];
    thread::spawn(move || { 
        //        ---- <-- Notice the move keyword!
        numbers[0] = 2;
        println!("The first number is now {}", numbers[0]);
    });

    // A subtle problem.. this push could cause the Vec to allocate
    // a new buffer. The other thread could read/write to this buffer
    // during this time, resulting in undefined behavior!
    numbers.push(20);
}
This dangerous code will not compile in Rust!

ReadWrite locks are a common and useful synchronization primitive, and Rust somewhat bakes this into the language. Holding an immutable reference to string is a bit like holding a read lock, and having a mutable reference is a bit like holding a write lock. However, all of this happens at compile time. Rust has a powerful static analysis system (people call it the “borrow-checker”) that ensures the written code never violates the constraint of "many readers, or one writer, but never at the same time."

The type system is even aware of “moving” data across threads, and can provide compile-time errors for when, for example, an OpenGL context gets shared across a thread boundary.

By the way, Rust's memory model also allows it to aggressively use noalias – so it's entirely plausible for Rust to be faster than C++ on average, despite using the same optimizer. (Well, if it weren't for bugs in llvm, that is.)

Free Lunch?

And here we come to the downside. Rust is not an easy language. The static analyzer must be able to understand your code, or you have to opt-out of the safety it provides. Lifetime annotation in Rust is powerful, but it is a new and complex language concept. Some designs work well with it, and some don’t. Even some "sound" designs are just not practical to use in Rust.

But it's not all bad. My experience so far has been that when code is sufficiently complicated that the borrow checker cannot validate it, it’s a code smell. But when the code can be validated, it very often works on the first try. That's a great property to have in a language! Designs that work well with the borrow checker are safe and tend to be very maintainable code.

Scalability

When I say “scalability” in the context of a language - what I mean is the ability of a codebase and team to change and grow in size, without diminishing returns of productivity. Requirements change, and code routinely needs to be refactored. Ideally, our tools for detecting errors in code are thorough enough that we can change our code without fear.

Dynamically-typed languages like python and javascript are not scalable. I'm amazed that the linting tools for python and javascript work as well as they do, but they simply cannot match a typesafe language's ability to find silly bugs as early as possible.

While C++ is not dynamically typed, it has it's own major scalability problem. Just as renaming a variable can cause unexpected runtime errors in a dynamically typed language, so also can altered memory access patterns cause unexpected runtime errors in a C++ application. This can lead to all sorts of unpleasant surprises at runtime.

Memory usage semantics should be part of the API, both for readability, and for compile-time verification. Here’s a C++ example to illustrate:

void the_function(char *buffer);
void the_function(char &buffer);
void the_function(std::unique_ptr<char *> buffer);
A few different ways to pass a pointer, each with their own semantics.

These are all different ways to effectively pass a pointer. But an experienced C++ developer could intuit certain things from the second and third case, where the first case would be ambiguous. Rust makes these semantics explicit, and enforces them.

This is not only beneficial to teams, but also the OSS community in general. When semantics for memory handling and concurrency are part of the API, breaking upstream changes are much more likely to be caught!

The Inflection Point

It’s easy to speak of Rust as a language, but it’s probably more correct to consider it as an ecosystem. For-profit ventures require libraries for common functionality, tools for enhancing productivity, and a talent-pool to hire from. Deficiencies in these and other areas represent risks to a project, and every organization has its own tolerance for risk.

However, I think this ecosystem is going to be big. As long as people enjoy working in it (and apparently they do, for four years running!), more libraries will be written, the tooling will get better, and the talent pool will grow. I’d like to see a bit more corporate investment, and not just from mozilla. But even in the last half year, there have been promising signs from Microsoft, Facebook and Amazon.

I think Rust is in an incredible position for exponential growth. I'm very excited about the ecosystem's future!