First Impressions of the Rust Programming Language

Posted on Fri, 8 Jun 2018 at 13:02:33 EST

Tagged aswriteup, rust ,programming, andsystems.

C is almost 50 years old, and C++ is almost 40 years old. While age is usually indicative of mature implementations with decades of optimization under their belts, it also means that the language's feature set is mostly devoid of modern advancements in programming language design. For that reason, you see a great deal of encouragement nowadays to move to newer languages - they're designed with contemporary platforms in mind, rather than working within the limitations of platforms like the PDP-11. Among said "new languages" are Zig, Myrddin, Go, Nim, D, Rust.. even languages like Java and Elixir that run on a virtual machine are occasionally suggested as alternatives to the AOT-compiled C and C++.

I have plans to look into the characteristics that distinguish each and every one of these new programming languages, learning them and documenting my first impressions in the form of blog posts. This post is the beginning of that adventure: my first impressions of Rust. I chose to evaluate Rust first rather than one of the other aforementioned contenders for a few reasons. For one, it's backed by some big names like Mozilla, so I'm expecting it to have more polished documentation than its independently developed counterparts - we might as well step off with a language that I can learn without needing to read the compiler's source code. Also, I've been fairly critical of Rust in the past because that view was in-line with the opinions of my friends, but now that I've decided to go out of my way to learn a new programming language, I might as well use this as an opportunity to see if my criticisms were unfounded.

Learning these new programming languages is certainly going to be an undertaking. Because Python and C were the first languages I was introduced to, I was able to simply buckle down, learn them, and apply them to pretty much everything I was doing at the time. When I tried to learn other languages later on, though, I had a hard time gauging whether or not I was making progress. I think that this is because I wasn't engaged with what I was learning; I was, at most, writing trivial programs with the language I was learning, and defaulting to C or Python whenever I needed to work on a "real" project. My goal is to learn these new languages to the extent that I can meaningfully evaluate them, so I've looked back on my past attempts and come to the conclusion that I either need to use them to develop something nontrivial, or make contributions to a free software project written in the language, as suggested by several articles . In the case of this post, it will be the former, as I've actually come to like Rust enough to use it for my reimplementation of Ken Silverman's BUILD engine .

With my introduction for this series out of the way, we can get into my first impressions of Rust. The first step was diving into the documentation to learn it, so I guess it would make sense to begin with that. Simply put, there is no shortage of high-quality learning material for Rust. "The Rust Programming Language," the equivalent of TCPL for Rust, is surprisingly well-written. Even if you're familiar with a systems programming language like C, I would still recommend reading it cover-to-cover. I had initially started off with the "Rust for C++ Programmers" and the "Learn X in Y Minutes" tutorial for Rust, but until I read TRPL, there was a lot that didn't make sense, and I was completely lost when it came to using the standard library. The book is friendly, encouraging, and full of great examples that outline common patterns in the standard library and various third party crates. My only real complaint with TRPL is that some of the the analogies step foot into the territory of monad tutorials . Some exceptional examples are comparing a reference-counting pointer to the TV in a family room , or comparing references to tables at a restaurant . They aren't all bad, and there are a few that I actually really enjoy, like the comparison of message passing concurrency to a river , but most of them try too hard to relate the concept to something in the real world that it ends up being unhelpful. Fortunately, the book is on GitHub and accepts pull requests, so I have plans to send in suggestions for some alternatives.

Despite the presence of great documentation, I predict that most people are still going to have a hard time learning Rust. It brings some concepts that you probably haven't seen before. As far as I'm aware, this is the first programming language to offer compile-time memory management. (C++ has smart pointers which are definitely similar, but those rules are enforced at runtime. Rust tightly integrates its concepts of ownership and lifetimes into the compiler.) TRPL does a good job of introducing the concepts for compile-time memory management, but I feel that that it only really scratches the surface. For that reason, I'd like to point anyone learning Rust to a great supplementary resource on the memory-model: "Learning Rust With Entirely Too Many Linked Lists" . It's hands-on, and just about as approachable as TRPL. This post might also help if you're having trouble grasping the general concept.

That brings me to another point - the features that Rust brings to the table might be difficult to learn, but learning to use them pays off in the end. Compile-time memory management requires designing your programs in a way you might not be used to, but it definitely beats manual memory management, or letting a runtime take care of garbage collection.

C's memory model, for example, is manually managed. Heap allocations are performed via malloc(3) and calloc(3), and those allocations exist until free(3) is called. Take this trivial piece of code for making a heap allocation containing a string:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv) {
    char *buf;

    // Make a heap allocation of 14 bytes.
    buf = calloc(14, 1);

    // calloc(3) CAN return a null pointer.
    if (buf == NULL) {
        return 1;

    // Fill the allocated buffer with a string, and print it.
    strcpy(buf, "Hello, world!");

    // Free the heap allocation, since we're done with it.
    // This won't always be at the end of the function, but it usually will be.

    return 0;

This model requires keeping track of the allocations you make and ensuring that they're freed when they aren't needed anymore - we easily could've forgotten that call to free(3). In this really trivial example, it doesn't matter because the process exits and the operating system reclaims the heap page, but if the program kept running after printing that string, we'd be dealing with a memory leak. Anyway, C's manual memory management is explicit enough that you can more or less predict what this will compile down to. GCC 6.4.0 emits following amd64 code:

# Prelude.
55             pushq %rbp
4889e5         movq %rsp, %rbp
4883ec20       subq $0x20, %rsp
897dec         movl %edi, -0x14(%rbp)
488975e0       movq %rsi, -0x20(%rbp)

              # calloc(14, 1), store pointer on the stack.
be01000000     movl $1, %esi
bf0e000000     movl $0xe, %edi
e892feffff     callq sym.imp.calloc
488945f8       movq %rax, -8(%rbp)

              # Check for null pointer.
48837df800     cmpq $0, -8(%rbp)
7507           jne 0x750
b801000000     movl $1, %eax
eb3b           jmp 0x78b

              # (Really optimized) call to strcpy.
488b45f8       movq -8(%rbp), %rax
48ba48656c6c.  movabsq $0x77202c6f6c6c6548, %rdx
488910         movq %rdx, 0(%rax)
c740086f726c.  movl $0x646c726f, 8(%rax)
66c7400c2100   movw $0x21, 0xc(%rax)

              # puts(buf)
488b45f8       movq -8(%rbp), %rax
4889c7         movq %rax, %rdi
e846feffff     callq sym.imp.puts

              # free(buf)
488b45f8       movq -8(%rbp), %rax
4889c7         movq %rax, %rdi
e82afeffff     callq

              # Teardown.
b800000000     movl $0, %eax
c9             leave
c3             retq
0f1f00         nopl 0(%rax)

The equivalent in Rust is similar, but as you'll see, we don't need to explicity free the heap allocation.

fn main() {
    let buf = Box::new("Hello, world!");
    println!("{}", buf);

rustc 1.25 compiles this down into the following amd64 code:

# Prelude.
4883ec78       subq $0x78, %rsp

              # Heap allocation, made by the 'std::boxed::Box' smart pointer.
              # 'calloc' returns NULL on failure, but in Rust the stdlib will panic and clean up for us. Here we're ALWAYS guaranteed a valid pointer.
b810000000     movl $0x10, %eax
89c7           movl %eax, %edi
b808000000     movl $8, %eax
89c6           movl %eax, %esi
e849faffff     callq sym.alloc::heap::exchange_malloc::h42fa40019bea1ed3

              # Haha, I guess we're storing a reference to the string constant instead of the string itself. Oh well. It should still do a decent job of illustrating heap allocation.
488d35b2dd05.  leaq str.Hello__world, %rsi    ; obj.str.0 ; 0x64900 ; "Hello, world!\n"
4889c7         movq %rax, %rdi
488930         movq %rsi, 0(%rax)
48c740080d00.  movq $0xd, 8(%rax)
48897c2418     movq %rdi, 0x18(%rsp)

              # Ignore this, these instructions are initially skipped by the jump.
eb0c           jmp 0x6b6f
488b7c2468     movq 0x68(%rsp), %rdi
e813f5ffff     callq sym.imp._Unwind_Resume
0f0b           ud2

              # Print the box, delegating out to the Display impl.
              # All of this seemingly complicated code is from the expansion of 'println!'.
488d442418     leaq 0x18(%rsp), %rax
4889442460     movq %rax, 0x60(%rsp)
488b7c2460     movq 0x60(%rsp), %rdi
488d35cbfcff.  leaq sym._alloc::boxed::Box_T__as_core::fmt::Display_::fmt::ha7602d90696e436c, %rsi
e896feffff     callq sym.core::fmt::ArgumentV1::new::hab3e8958fe8b6def
4889542410     movq %rdx, 0x10(%rsp)
4889442408     movq %rax, 8(%rsp)
eb00           jmp 0x6b96
488b442408     movq 8(%rsp), %rax
4889442450     movq %rax, 0x50(%rsp)
488b4c2410     movq 0x10(%rsp), %rcx
48894c2458     movq %rcx, 0x58(%rsp)
4c8b0daf4727.  movq 0x0027b360, %r9
4889e2         movq %rsp, %rdx
48c702010000.  movq $1, 0(%rdx)
488d357e4727.  leaq obj.str.1, %rsi
bf02000000     movl $2, %edi
89fa           movl %edi, %edx
bf01000000     movl $1, %edi
4189f8         movl %edi, %r8d
488d7c2420     leaq 0x20(%rsp), %rdi
488d4c2450     leaq 0x50(%rsp), %rcx
e890feffff     callq sym.core::fmt::Arguments::new_v1_formatted::h13ca93c140433ddb

              # Ignore this, these instructions are initially skipped by the jump.
eb0f           jmp 0x6bf1
488d7c2418     leaq 0x18(%rsp), %rdi
e824f8ffff     callq sym.core::ptr::drop_in_place::he05ad455338a1c4f
e972ffffff     jmp 0x6b63

              # More code from 'println!'
488d7c2420     leaq 0x20(%rsp), %rdi
e8554c0000     callq sym.std::io::stdio::_print::hde6fc8f0049b89f6
eb00           jmp 0x6bfd

              # Finally, we're at the point where the heap allocation is freed.
              # This calls 'drop', which for 'std::boxed::Box' will deallocate the heap chunk.
488d7c2418     leaq 0x18(%rsp), %rdi
e809f8ffff     callq sym.core::ptr::drop_in_place::he05ad455338a1c4f

              # Teardown.
4883c478       addq $0x78, %rsp
c3             retq

              # This is error handling and calls the past few sections of code that I told you to ignore.
89d1           movl %edx, %ecx
4889442468     movq %rax, 0x68(%rsp)
894c2470       movl %ecx, 0x70(%rsp)
ebc9           jmp 0x6be2
0f1f80000000.  nopl 0(%rax)

Despite the fact that there's more code here as a result of Rust's panic handling and the complexity of the 'println!' macro, rustc's emitted assembly does pretty much the same thing as that of GCC - allocate a buffer, fill it, then free it. Rust just façades this process with a friendlier abstraction.

Another feature I've come to really enjoy is that there are no more NULL pointers - they've been replaced by a strict type system à la Haskell. In the C example above, we saw that calloc(3) can return NULL if glibc isn't able to allocate enough memory. We easily could've forgotten to put in the check to make sure the it isn't NULL, in which case we would get a segmentation fault. Preventing this sort of thing is what people are talking about when they say "memory safety." For a segmentation fault, the operating system has to jump in because we're doing something we shouldn't - dereferencing a NULL pointer. There are plenty of other naughty things we can do in C, like freeing a heap allocation twice, or even worse, writing outside the bounds of a buffer. Rust aims to have the compiler step in when we do something dumb, rather than leaving that to the operating system or exploit mitigation systems. To do this for NULL-able references, Rust provides an "Option" type (and the "Result" type) that can represent either something or nothing. You see it used extensively in the standard library. Consider the 'find' method of std::string::String, a method for finding the index of a substring in a string. There's the possibility that the substring exists in the string, in which case we'd just return that index, but what if it doesn't exist? In the case of C, we might return some silly value like '-1', but in Rust, we return an Option<usize> - either some usize value, or nothing. And the compiler makes sure we understand the implications of this.

fn main() {
    let to_search = String::from("I may contain foo.");
    let index = to_search.find("foo");
    println!("index - 5: {}", index - 5);

This is a pretty inane example, but please bear with me. If we try to compile this, rustc errors out, because we're trying to treat a variable that might represent nothing as if it were guaranteed to be something.

error[E0369]: binary operation `-` cannot be applied to type `std::option::Option<usize>`
 4 |     println!("index - 5: {}", index - 5);
   |                               ^^^^^^^^^
   = note: an implementation of `std::ops::Sub` might be missing for `std::option::Option<usize>`

You might expect this strictness to bring frustration, but the compiler emits errors worded simply enough that a layman could understand them, and often makes suggestions for fixing the code in question. The above isn't a great example, here's a better one:

fn tabulate_slice(slice: &[u8]) {
    for elem in slice.iter() {
        println!("{}", elem);

fn main() {
    let vec = vec![1, 2, 3];
error[E0308]: mismatched types
 9 |     tabulate_slice(vec);
   |                    ^^^
   |                    |
   |                    expected &[u8], found struct `std::vec::Vec`
   |                    help: consider borrowing here: `&vec`

Rust has a great deal of functionality that makes it feel like your typical high-level Ruby or Python, despite being a compiled language. And it isn't limited to what I described above - here are a few of the other features I was really impressed with:

Conditionals are Expressions

let var = if true {
} else {

No parentheses for the expression part of if/while/for

Heh, I bet you've seen enough of that already.

Semantics for Infinite Loops

loop {

Semantics for Unused Variables/Parameters

for _ in 0..5 {
    println!("I'm printed 5 times!");

Range Notation, Type Inference, and Iterators

Again, you've seen these already.

Tuples, Destructuring, and Pattern Matching via match and if let Expressions

match to_search.find("foo") {
    Some(index) => println!("Foo at {}", index),
    None => println!("No foo :("),

// Or, more idiomatically:

if let Some(index) = to_search.find("foo") {
    println!("Foo at {}", index);
} else {
    println!("No foo :(");

Automated testing is integrated into the build system

mod tests {
    fn it_works() {
        assert_eq!(2 + 2, 4);

That's my opinion on the language design aspect, but the community and ecosystem are important as well. My experience with the Rust community is limited, but from what little I have seen, those in the community are friendly and rational. I submitted a few issues to rust-imap and received prompt and helpful responses. I can also confidently say that the Rust ecosystem a pleasure to work with. It obviously isn't as mature as some other language ecosystems, but adding a "crate" dependency to your projects is as easy as adding a line to your 'Cargo.toml'. It's equally easy to publish the code and documentation for crates you've made yourself. I threw together a library for interacting with WildMIDI , and a page popped up without any intervention from me. Painless.

The process of linking those crates into the executable is relatively primitive, and there are a few complaints in that respect. It's mostly static linking, so the argument is "you get outdated copies of several libraries on your computer." However, the benefits of dynamic linking as the alternative is a debate I don't want to get into in this post . Right now I'll leave it as, "it's not an option in the current implementation, and that's a disadvantage," even if I'm blissfully ignorant of the size of my Rust binaries and might have some complaints about dynamic linking.

All in all, I'm very happy with Rust. Maybe it isn't "there" yet as a viable replacement to C, but it's promising and I have a feeling that, with time, it will fit nicely into GNU/Linux ecosystem.