pcwalton

Occasional notes on Rust, Firefox, etc.

Performance of Sequential Rust Programs

| Comments

Although Rust is designed for parallel programs, it is important that the performance of single-threaded, sequential programs not suffer in its design. As far as Servo is concerned, sequential performance is still important in many domains that a Web browser engine must compete in.

Below are some selected single-threaded benchmarks from the Computer Language Benchmarks Game (formerly, and still informally, called the “shootout”). This is far from an ideal set. These benchmarks are showing their age quite heavily, they are too small and simplistic to extrapolate to real-world use cases, and many of them are too I/O-bound.

It is perfectly legal per the rules of the benchmarks game to use unsafe code (or calling libraries written in C, which is equivalent), and I believe it’s very difficult to precisely match C’s performance without resorting to unsafe code. (Practically speaking, one would need an extremely smart JIT, or a research language with a complex dependent type system.) As my colleague Niko pointed out, a more interesting benchmark would not allow any languages to use unsafe code and would exclude C and C++ from competing at all, except as a point of comparison—such a benchmark would be interesting to determine how much performance one has to trade for type safety in mainstream languages. But the shootout is what it is, and so the Rust versions of these benchmarks heavily use unsafe code. Over time, I hope to be able to reduce the amount of unsafe code present in the Rust versions of these benchmarks, but a couple of benchmarks will likely always remain unsafe.

Neither the C nor the Rust versions of these benchmarks use SIMD or threads. This is by design, as the goal of this test is to measure Rust’s sequential performance. Over time, as Rust gains SIMD support and the scheduler improves (both of which are active areas of development), the benchmarks will be updated to use multiple threads. But keep in mind that the C implementation tested against is not usually the top one on the shootout site; rather, I selected the fastest implementation that did not use SIMD or threads for comparison. As the Rust benchmarks are updated to use SIMD and threads, equivalent C versions will be used for comparison.

For all these reasons and more, it is important to not read too much into these benchmark results. It would be a mistake to conclude that “Rust is faster than C” because of the performance on the k-nucleotide benchmark. Likewise, it would be a mistake to conclude that “C is faster than Rust” because of the fasta-redux benchmark. The goal here is simply to demonstrate that sequential Rust can be written in a way that approaches competitive parity with equivalent C code.

Note that the benchmarks include clang because GCC 4.2 is a very old version. The purpose of this benchmark is not to benchmark C compilers, but rather to perform cross-implementation comparisons between two languages.

Enough disclaimers; on to the results:

Results

These programs were tested on a 2.53 GHz Intel Core 2 Duo MacBook Pro with 4 GB of RAM, running Mac OS X 10.6 Snow Leopard. “GCC 4.2” is GCC 4.2.1, Apple build 5666; “clang 1.7” is Apple clang 1.7, based on LLVM 2.9svn; “clang 3.1” is LLVM 3.1, trunk 149587. GCC and clang were run with -O2, and Rust was run with -O (which is like -O2). Three runs were averaged together to produce each result. Results are normalized to GCC 4.2. Lower numbers are better.

As mentioned before, this is a selected set of benchmarks. The benchmarks that were not included are:

  • fasta is omitted because it is similar to fasta-redux.

  • regexp-dna is omitted because it consists of an uninteresting binding to PCRE.

  • binary-trees is omitted because it is a garbage collection benchmark and the C version uses an arena, defeating the purpose (although I suspect a Rust version that did the same would do well).

  • chameneos-redux and threadring are omitted because they are threading benchmarks.

You can see the changes to the Rust compiler that were made to optimize these tests, as well as the benchmark sources, on my branch of the compiler on GitHub. The goal will be to land these changes over the next few days.

A Hard Case for Memory Safety

| Comments

Quick quiz: In this C++ program, is the definition of munge guaranteed to be memory safe? (Assume that the definition of increment_counter uses only modern C++ idioms and doesn’t do anything like dereference an invalid pointer.)

#include <iostream>
#include <vector>

class foo {
public:
    std::vector<int> indices;
    int counter;

    foo() : indices(), counter(0) {
        indices.push_back(1);
        indices.push_back(2);
        indices.push_back(3);
    }

    void increment_counter();

    int &get_first_index() {
        assert(indices.size() > 0);
        return indices[0];
    }

    void munge() {
        int &first = get_first_index();
        increment_counter();
        std::cout << first << std::endl;
        first = 20;
    }
};

int main() {
    foo foo;
    foo.munge();
    return 0;
}

The answer: Even with this caveat, we can’t tell! It depends on the definition of increment_counter.

If increment_counter has this definition, the code is memory safe:

void foo::increment_counter() {
    counter++;
}

But if increment_counter has this definition, for example, then it isn’t:

void foo::increment_counter() {
    indices.clear();
    counter++;
}

This definition would cause the first reference in munge to become a dangling reference, and the call to std::cout and subsequent assignment of first will have undefined behavior. If first were not an int but were instead an instance of a class, and munge attempted to perform a virtual method call on it, then this would constitute a critical security vulnerability.

The point here is that determining memory safety in C++ requires non-local reasoning. Any analysis that tries to determine safety of C++ code, whether performed by a machine or performed by a human auditor, has to analyze many functions all at once, rather than one function at a time, to determine whether the code is memory safe. As this example illustrates, sticking to modern C++ coding styles, even with bounds checks, is not enough to prevent this.

There are a few ways around this:

  • For each function call, analyze the source to the called function to determine whether it’s memory safe in the context of the caller. This doesn’t always work, though: it’s hard or impossible when function pointers or virtual methods are involved (which function ends up being called?), and it’s hard with separately compiled code (what if the called function is in a DLL that you don’t have source for?)

  • Change the type of indices to std::vector<std::shared_ptr<int>>; i.e. use reference counting to keep the pointer alive. This has a runtime cost.

  • Inline the body of increment_counter, so that the memory safety of munge is immediately clear.

  • Make increment_counter a class method (or just a function) instead of an instance method, and have it take counter by reference. The idea here is to prevent the possibility that increment_counter could mess with indices in any way by shutting off its access to it.

What does this have to do with Rust? In fact, this error corresponds to a borrow check error that Brian Anderson hit when working on the scheduler. In Rust, the corresponding code looks something like this:

impl Foo {
    fn get_first_index(&'a mut self) -> &'a mut int {
        assert!(self.indices.len() > 0);
        return &mut indices[0];
    }

    fn munge(&mut self) {
        let first = self.get_first_index();
        self.increment_counter(); // ERROR
        println(first.to_str());
        *first = 20;
    }
}

This causes a borrow check error because the first reference conflicts with the call to increment_counter. The reason the borrow check complains is that the borrow check only checks one function at a time, and it could tell (quite rightly!) that the call to increment_counter might be unsafe. The solution is to make increment_counter a static method that only has access to counter; i.e. to rewrite the self.increment_counter() line as follows:

Foo::increment_counter(&mut self.counter);

Since the borrow check now sees that increment_counter couldn’t possibly destroy the first reference, it now accepts the code.

Fortunately, such borrow check errors are not as common anymore, with the new simpler borrow check rules. But it’s interesting to see that, when they do come up, they’re warning about real problems that affect any language with manual memory management. In the C++ code above, most programmers probably wouldn’t notice the fact that the memory safety of munge depends on the definition of increment_counter. The challenge in Rust, then, will be to make the error messages comprehensible enough to allow programmers to understand what the borrow checker is warning about and how to fix any problems that arise.

An Overview of Memory Management in Rust

| Comments

One of the key features of Rust that sets it apart from other new languages is that its memory management is manual—the programmer has explicit control over where and how memory is allocated and deallocated. In this regard, Rust is much more like C++ than like Java, Python, or Go, to name a few. This is an important design decision that makes Rust able to function in performance-critical domains that safe languages previously haven’t been able to—top-of-the line games and Web browsers, for example—but it adds a nontrivial learning curve to the language.

For programmers familiar with modern C++, this learning curve is much shallower, but for those who are used to other languages, Rust’s smart pointers can seem confusing and complex. In keeping with the systems-oriented nature of Rust, this post is designed to explain how Rust’s memory management works and how to effectively use it.

Smart pointers

In many languages with manual memory management, like C, you directly allocate and free memory with calls to special functions. For example:

void f() {
    int *x = malloc(sizeof(int));  /* allocates space for an int on the heap */
    *x = 1024;                     /* initialize the value */
    printf("%d\n", *x);            /* print it on the screen */
    free(x);                       /* free the memory, returning it to the heap */
}

C gives you a great deal of control over where memory is allocated and deallocated. Memory is allocated with a special function malloc, and it is freed with a special function free. After the call to free, it is an error to attempt to use x, as it is a dangling pointer. A dangling pointer points to invalid memory, but the C compiler makes no attempt to prevent you from using it; it’s your responsibility to avoid touching it after freeing the memory it points to.

Rust gives you the same level of control over memory, but it works somewhat differently. Let’s see how the same piece of code looks in Rust:

fn f() {
    let x: ~int = ~1024;          // allocate space and initialize an int
                                  // on the heap
    println(fmt!("%d", *x));      // print it on the screen
} // <-- the memory that x pointed at is automatically freed here

There are three main differences to notice here:

  1. In C, you allocate memory first (with the call to malloc), and then you initialize it (in the example above, with the *x = 1024 assignment). Rust fuses the two operations together into the ~ allocation operator, so that you don’t accidentally forget to initialize memory before you use it.

  2. In C, the call to malloc returns a plain pointer, int *. In Rust, the ~ operator, which allocates memory, returns a special smart pointer to an int. Because this type of smart pointer is so common, its name is just a single character, ~—thus the type of this smart pointer is written as ~int.

  3. You don’t call free manually in Rust. Rather, the compiler automatically frees the memory for you when a smart pointer goes out of scope.

As it turns out, points (2) and (3) are very intertwined, and together they form the cornerstone of Rust’s memory management system. Here’s the idea: Unlike C, allocation functions in Rust don’t return a raw pointer to the space they allocate. Instead, they return a smart pointer to the space. A smart pointer is a special kind of value that controls when the object is freed. Like a raw pointer in C, you can access the data that a smart pointer refers to with *. But unlike a raw pointer, when the smart pointer to an allocation goes out of scope, that allocation is automatically freed. In this way, smart pointers are “smart” because they not only track where an object is but also track how to clean it up.

Unlike C, in Rust you never call free directly. Instead, you rely on smart pointers to free all allocations. The most basic reason for this is that smart pointers make it harder to forget to free memory. In C, if you forget to call free, you have a memory leak, which means that the memory will not be cleaned up until the program exits. However, in Rust, the compiler will automatically insert the code necessary to free the memory for you when the smart pointer pointing to your data goes out of scope.

Rust has multiple types of smart pointers, corresponding to the different strategies that programs use to reclaim memory. Some smart pointers, namely ~ and @ (which we will cover shortly), have special names known to the compiler, because they’re so common. (Having to type long names like unique_ptr all the time would be a burden.) Other smart pointers, such as ARC (which allows you to share read-only data between threads), are in the standard library and are not built into the compiler.

The pointer covered above is known as the unique smart pointer ~. We call it “unique” because there is always only one smart pointer pointing to each allocation. The other type of smart pointer built into the language is the managed smart pointer, which allows multiple smart pointers to point to the same allocation and uses garbage collection to determine when to free it. Here’s an example of a managed smart pointer in use:

fn foo() {
    let x: @int = @1024;     // allocate space and initialize an int
                             // on the heap
    bar(x);                  // pass it to `bar`
    println(fmt!("%d", *x)); // print it on the screen
} // <-- the memory can be freed here

fn bar(x: @int) {
    let y: @int = x;         // make a new smart pointer to `x`
} // <-- despite `y` going out of scope, the memory is *not* freed here

The key difference between ~ and @ is that @ allows multiple smart pointers to point to the same data, and the data is cleaned up only after the last such smart pointer disappears. Notice that, in this example, the memory pointed at by y (which is the same as the memory pointed at by x) is not freed at the end of the function bar, because x is still in use and also points to the same data. The fact that @ allows multiple smart pointers to the same data, as well as the fact that the allocation is freed only when all of those pointers go out of scope, make managed smart pointers very useful. However, they can be less efficient than unique smart pointers, as they require garbage collection at runtime.

References

Recall that a smart pointer is a pointer that automatically frees the memory that it points to when it goes out of scope. Perhaps surprisingly, it often turns out that it’s useful to have a kind of pointer that doesn’t free the memory that it points to. Consider this code:

struct Dog {
    name: ~str    // a unique smart pointer to a string
}

fn dogshow() {
    let dogs: [~Dog * 3] = [        // create an array of Dog objects
        ~Dog { name: ~"Spot"   },   // use unique smart pointers to
                                    // allocate
        ~Dog { name: ~"Fido"   },
        ~Dog { name: ~"Snoopy" },
    ];
    for dogs.each |dog| {
        println(fmt!("Say hello to %s", dog.name));
    }
} // <-- all dogs destroyed here

Suppose that we wanted to single Fido out as the winner of the dog show. We might try this code:

fn dogshow() {
    let dogs: [~Dog * 3] = [
        ~Dog { name: ~"Spot"   },
        ~Dog { name: ~"Fido"   },
        ~Dog { name: ~"Snoopy" },
    ];
    let winner: ~Dog = dogs[1];
    for dogs.each |dog| {
        println(fmt!("Say hello to %s", dog.name));
    }
    println(fmt!("And the winner is: %s!", winner.name));
} // <-- all dogs, and `winner`, destroyed here

But this code won’t compile. The reason is that, if it did, Fido would be destroyed twice. Remember that unique smart pointers free the allocations they point to when they go out of scope. The code attempts to make a second smart pointer to Fido at the time it executes the line let winner: ~Dog = dogs[1]; If the compiler allowed this to proceed, then at the end of the block, the program would attempt to free Fido twice—once when it frees the original smart pointer embedded within the dogs array, and once when it frees winner.

What we really want is for winner to be a pointer that doesn’t free the allocation that it points to. In fact, what we want isn’t a smart pointer at all; we want a reference. Here’s the code rewritten to use one:

fn dogshow() {
    let dogs: [~Dog * 3] = [
        ~Dog { name: ~"Spot"   },
        ~Dog { name: ~"Fido"   },
        ~Dog { name: ~"Snoopy" },
    ];
    let winner: &Dog = dogs[1];  // note use of `&` to form a reference
    for dogs.each |dog| {
        println(fmt!("Say hello to %s", dog.name));
    }
    println(fmt!("And the winner is: %s!", winner.name));
} // <-- all dogs destroyed here

This code will now compile. Here, we convert winner into a reference, notated in Rust with &. You can take a reference to any smart pointer type in Rust by simply assigning it to a value with a reference type, as the let winner: &Dog = dogs[1] line does.

References (also known as borrowed pointers) don’t cause the compiler to free the data they refer to. However, they don’t prevent the compiler from freeing anything either. They have no effect on what smart pointers will do; regardless of how many references you have, a unique smart pointer will always free the data that it points to when it goes out of scope, and a managed smart pointer will always free its data when all managed smart pointers to the same allocation go out of scope.

This is important to keep in mind. Code like this will not compile:

fn foo() {
    let y: &int;
    {
        let x: ~int = ~2048;
        y = x;
    } // <-- x freed here
    println(fmt!("Your lucky number is: %d", *y)); // ERROR: accesses freed data!
}

In languages like C++, code like this could cause faults from attempting to access invalid memory. As it turns out, however, this piece of code won’t compile—the Rust compiler can and does prevent you from writing code like this at compile time. Essentially, the Rust compiler tracks where each reference came from and reports an error if a reference persists longer than the allocation it points into. This means that, generally speaking, you can use references all you like and have the confidence that they won’t result in hard-to-diagnose errors at runtime.

Conclusion

These ideas—smart pointers and references—form the basis of memory management in Rust. If you’re a C++ programmer, most of this will (hopefully!) simply have been an exercise in learning different syntax. For other programmers, these concepts are likely more foreign. But using these tools, you can write code with fine-grained control over memory, with improved safety over languages like C.

Which Pointer Should I Use?

| Comments

Deciding whether to use a managed @ pointer or an owned ~ pointer to allocate memory is one of the most frequent sources of confusion for newcomers to Rust. There are two main angles to consider when deciding whether to use an @ pointer or a ~ pointer in Rust: memory management and concurrency. I’ll cover each in turn.

Note that this tutorial only presents the basic system. There are many extensions to the system—borrowing, library smart pointers, cells, and so on—that allow the various limitations described here to be overcome. But this is the core system that needs to be understood first.

Memory management

One of the most important features of Rust from a systems programming perspective is that garbage collection is optional. What this means is that there are safe ways to allocate memory that do not require bookkeeping at runtime to determine when it is safe to free that memory.

What makes it possible for Rust programs to avoid runtime garbage collection is the notion of ownership of a particular allocation. Under this scheme, when the single owner of an allocation goes out of scope, the allocation is freed. Owned pointers in Rust are notated with ~. Here’s an example of their use:

struct Point {
    x: int,
    y: int,
}

fn f() {
    let x: ~Point = ~Point { x: 10, y: 20 };  // allocate a Point on the heap
}  // <-- x is freed here

Here, x is the single owner of the Point on the heap. Because there is only a single owner, Rust can throw away the memory pointed to by x at the end of the function.

The compiler enforces that there is only a single owner. Assigning the pointer to a new location transfers ownership (known as a move for short). Consider this program:

fn g() {
    let a: ~Point = ~Point { x: 10, y: 20 }; // allocate a Point on the heap
    let b = a;                               // now b is the owner
    println(b.x.to_str());                   // OK
    println(a.x.to_str());                   // ERROR: use of moved value
} // <-- b is freed here

When compiling this program, the compiler produces the error “use of moved value”. This is because assigning an owned pointer transfers ownership, making the old variable dead. Because the compiler knows precisely which variables are dead at all times, it can avoid having to determine at runtime whether to free the memory that a variable points to, and it can prevent you from accidentally accessing dead variables. However, this comes at a price: you are limited to using a single variable to refer to an ~ allocation.

By contrast, @ pointers do not have this limitation. We think of memory that is allocated with @ as owned by the garbage collector. You can make as many pointers to @ memory as you would like. There is a cost in runtime performance, but this cost comes with a great deal of flexibility. For example, the code above will compile with an @ pointer:

fn h() {
    let a: @Point = @Point { x: 10, y: 20 }; // allocate a Point on the heap
    let b = a;                               // a and b share a reference
    println(b.x.to_str());                   // OK
    println(a.x.to_str());                   // also OK
}

So, in short: @ pointers require garbage collection, but allow multiple pointers to the same location. ~ pointers avoid this GC overhead, but they don’t allow multiple pointers to the same location.

Concurrency

Another equally important aspect to the distinction between @ and ~ is that it ensures that concurrent Rust tasks don’t race on shared memory. To illustrate this, here’s an example of broken code that doesn’t compile:

struct Counter {
    count: int
}

fn f() {
    // Allocate a mutable counter.
    let counter: @mut Counter = @mut Counter { count: 0 };
    do spawn {               // spawn a new thread
        // Increment the counter.
        counter.count += 1;  // ERROR: attempt to capture an `@` value
    }
    println(counter.count.to_str()); // print the value
}

This code contains a classic race—if this code compiled, then the value printed would be either 0 or 1, depending on whether the counter.count += 1 line executed first or the println executed first. The key here is that two threads—the spawned thread and the main thread—are both simultaneously attempting to access the counter object. To prevent these errors, Rust prevents multiple threads from accessing the same memory at the same time.

Recall from the previous section that there can be any number of pointers to memory allocated with @. But there can be only one pointer to memory allocated with ~. This suggests a way to forbid multiple threads from accessing the same data: restrict the types of pointers that can be sent between threads to ~ pointers. And this is exactly what Rust does.

For any piece of ~-allocated memory, there is only one pointer to it, and that pointer is owned by exactly one thread. So there can be no races, since any other threads simply don’t have access to that memory. Let’s rewrite our example above using ~ to illustrate this:

fn g() {
    // Allocate a mutable counter.
    let mut counter: ~Counter = ~Counter { count: 0 };
    do spawn {               // spawn a new thread
        counter.count += 1;  // increment the counter
    }
    println(counter.count.to_str()); // ERROR: use of moved value
}

What’s going on here is that, by referring to counter inside the spawn block, the new thread takes ownership of the counter variable, and the counter variable becomes dead everywhere outside that block. Essentially, the main thread loses access to counter by giving it away to the thread it spawns. So the attempt to print the value on the screen from the main thread will fail. By contrast, this code will work:

fn h() {
    // Allocate a mutable counter.
    let mut counter: ~Counter = ~Counter { count: 0 };
    do spawn {               // spawn a new thread
        counter.count += 1;  // increment the counter
        println(counter.count.to_str()); // OK: `counter` is owned by this thread
    }
}

Notice that the data race is gone: this code always prints 1, because the printing happens in the thread that owns the Counter object.

The resulting rule is pretty simple. In short: @ pointers may not be sent from thread to thread. ~ pointers may be sent, and are owned by exactly one thread at a time. Therefore, if you need data to be sent, do not allocate it with @.

Conclusion (TL;DR)

So the distinction between @ and ~ is often confusing to newcomers, but it’s really quite simple. There are two main rules to remember:

  1. ~ only supports one pointer to each allocation, so if you need multiple pointers to the same data, use @. But @ requires garbage collection overhead, so if this is important to your application, use ~ wherever possible.

  2. Don’t use @ pointers if you need to send data between multiple threads. Use ~ instead.

Finally, I should note again that, if these rules are too restrictive for you (for example, if you need multiple pointers but can’t tolerate garbage collection pauses), there are more advanced solutions: borrowing, safe smart pointers, and unsafe code. But this simple system works well for many programs and forms the foundation of Rust’s approach to memory management.

The New Borrow Check in a Nutshell

| Comments

If you’ve used Rust for any period of time, you’ve probably been bitten by the mysterious borrow check—the compiler pass responsible for preventing iterator invalidation, as well as a few other dangling pointer scenarios. The current iteration of the borrow check enforces a fairly complex set of rules. Because the rules were hard to understand and ruled out too many valid programs, we were never really satisfied with the analysis; without a simple set of rules to follow, programmers will get frustrated and give up. To remedy this, Niko has proposed a revamp of the borrow checker known as “Imagine Never Hearing the Phrase ‘Aliasable, Mutable’ Again”. This has mostly been implemented in a pull request now, so I’d like to take the opportunity to explain the new rules. I’m particularly excited about this change because now the entire set of borrow check rules are simple enough to boil down to one principle.

Here’s the rule that the new borrow check is in charge of enforcing: Whenever you take a pointer to an object, you may not modify that object as long as that pointer exists, except through that pointer.

(Strictly speaking, this is not all the new borrow check enforces, but the other errors the pass can produce are generally straightforward and simple dangling pointer errors. Also, I’m omitting the rules related to &const, as this rarely-used type of pointer is likely to be removed.)

For unique pointers (~) and borrowed pointers (&), this rule is enforced at compile time, without any runtime overhead. Here’s an example:

let mut the_magic_word = Some(~"zap");
match the_magic_word {
    None => {}
    Some(ref word) {
        the_magic_word = None; // ERROR
        io::println(*word);
    }
}

Here, the line marked ERROR produces the error “assigning to mutable local variable prohibited due to outstanding loan”. This happens because we violated the rule above—the line the_magic_word = None mutates the value the_magic_word while there exists a pointer to it (word).

Another example:

struct Foo {
    array: ~[int]
}

impl Foo {
    fn bar(&mut self) {
        for self.array.each |i| {
            self.array = ~[];  // ERROR
            io::println(i.to_str());
        }
    }
}

Again, the error is “assigning to mutable field prohibited due to outstanding loan”. As before, it’s traceable to a violation of the mutation rule: the line self.array = ~[] mutates the self.array field while a pointer (i) into it exists.

This example is interesting for a couple of reasons. First of all, it illustrates the way the Rust compiler can catch iterator invalidation issues without runtime overhead in many cases: here the compiler is able to detect that the i iterator, which has type &int, was invalidated, and rejects the program instead of permitting undefined behavior at runtime. Second, this example illustrates something not possible under the current borrow check regime that the new borrow check allows: namely, taking an immutable pointer to a field accessible through a &mut pointer. (An immutable pointer is needed to call the each method to prevent iterator invalidation.) More than any other, this restriction probably led to the greatest number of borrow check errors in practice, since it prevented iterating over any collections reachable from &mut pointers.

Now all of this works fine for & and ~ pointers, but what about managed boxes (@)? It turns out that immutable @ boxes are easy to deal with; since they can’t be mutated at all, the borrow checker doesn’t have to do anything to enforce the no-mutation rule. However, for @mut boxes, the situation is more complicated. For @mut boxes, the new borrow checker inserts runtime checks to enforce the pointer rules. Attempting to mutate an @mut box while a pointer to its contents exists results in task failure at runtime, unless the mutation is done through that pointer.

Interestingly, this is similar to the way various debug or safe STL implementations (for example, Microsoft’s) guard against iterator invalidation. The differences are: (1) in Rust, the checks are automatically inserted by the compiler instead of built into each collection by hand; and (2) the checks are only needed for garbage collected data, as the compiler can perform the checks at compile time for other types of data.

There is one gotcha here, however. As implemented, if any pointer exists to any part of an @mut box, then the entire box cannot be mutated while that pointer exists. This means that this example will fail:

struct Dungeon {
    monsters: ~[Monster],
    total_gold: int
}

impl Dungeon {
    fn count_gold(@mut self) { // note `@mut self`, not `&mut self`
        self.total_gold = 0;
        for self.monsters.each |monster| { // pointer created here
            self.total_gold += monster.gold;
        }
    }
}

Note that the iterator variable monster has type &Monster. This is a pointer to the inside of Dungeon, so the assignment to self.total_gold violates the mutation rule. Unfortunately, the compiler does not currently catch this, so the program will fail at runtime.

There are a couple of workarounds. The simplest way is to change @mut self to &mut self. Since there is no need to give out the @mut pointer for this operation, this is safe. Roughly speaking, the compile-time checks operate on a per-field basis, while the runtime checks operate on a per-box basis. So this change makes the operation succeed. Another possibility is to make total_gold into a local variable and assign to the field after the for loop.

Despite the fact that this error is easy to fix, I’m concerned about the fact that the compiler won’t catch this kind of thing at compile time. So I think we should introduce a set of warnings that looks for common violations of this rule. It’s impossible to make the warnings catch all failures—that’s the reason why the check is done at runtime in the first place. (In general, trying to make the compiler reason about @ boxes is hard, since the compiler has no idea how many references to them exist.) But I suspect that we could make the analysis good enough to catch the majority of these errors in practice.

In any case, the take-away from all of this is that the borrow checker should be much easier and more transparent with this change. There’s essentially just one straightforward rule to remember.

The Two Meanings of “Impl”

| Comments

impl declarations in Rust have two forms. The subtle distinction between the two can be confusing at first, so I’ll briefly explain the difference here.

The first form of impl is a type implementation. (Earlier I was calling this an “anonymous trait”, but I think that this terminology is probably more confusing than it’s worth.) This form allows you to define new functions associated with a type. For example:

struct Dog {
    name: ~str
}

impl Dog {
    static fn new(name: ~str) -> Dog {
        return Dog { name: name };
    }

    fn speak(&self) {
        io::println("woof");
    }
}

This example defines new functions new and speak under the Dog namespace. Here’s an example of their use:

let dog = Dog::new("Snoopy");
Dog::speak(&dog); // note: doesn't work today, see note below

(The explicit call of the form Dog::speak(&dog) doesn’t work today, but I wrote it out to emphasize the fact that speak lives in the Dog namespace. It’s likely to work in the future, though. Today, you need to write dog.speak().)

The second form of impl, on the other hand, is a trait implementation. It’s distinguished from the first form by the presence of a : followed by the name of a trait. This form allows you to provide an implementation for one or more existing functions belonging to a trait. It doesn’t define any new functions. For instance, suppose I defined this trait:

trait Animal {
    static fn species(&self) -> ~str;
}

Then I can supply an implementation of species() for my Dog structure like this:

impl Dog : Animal {
    static fn species(&self) -> ~str {
        return ~"Canis lupus familiaris";
    }
}

The key point to notice here is that this form doesn’t define any new names. This code won’t compile:

let dog = Dog::new("Fido");
io::println(Dog::species(&dog)); // unresolved name: `species`

But this code will:

let dog = Dog::new("Spot");
io::println(Animal::species(&dog));

The reason is that a trait implementation only provides the implementation of one or more existing functions rather than defining new functions. The function species is part of the Animal trait; it’s not part of Dog.

(You might reasonably ask: Why not duplicate the name species into Dog, for convenience? The reason is because of name collisions: it should be possible to implement Animal and later implement another trait with a different function called species without breaking existing code.)

So the upshot of this is that there are two forms of implementations in Rust: the type implementation, which defines new functions, and the trait implementation, which attaches functionality to existing functions. Both use the impl keyword, but they’re different forms with different meanings.

A Tour of Vector Representations

| Comments

One aspect of Rust that’s often confusing to newcomers is its treatment of strings and vectors (also known as arrays or lists). As a result of its focus on systems programming, Rust has a somewhat lower-level concept of a vector than most other languages do. As part of an overall goal to make Rust easy to understand, I thought I’d write up a quick tour of the way other languages’ vectors work from the perspective of the machine in order to make it easier to map these concepts into Rust.

There are three common models that I’ve observed in use—for lack of better terminology, I’ll call them the Java model, the Python model, and the C++ STL model. (For brevity, I’ve omitted fixed-size, stack-allocated arrays, since these are very limited.) Most languages build upon one of these three. In a subsequent blog post, I’ll explain how Rust’s system differs from these and how the programmer can build the equivalents of each of these models in Rust.

We’ll start with the Java model. Java’s basic array type has a fixed size when created and cannot be changed afterward. Arrays in Java are always allocated on the Java heap. For example, consider the following line of code:

int[] a = { 1, 2, 3, 4, 5 };

After this code is executed, the memory of the running program looks like this:

image

The cell highlighted in red is the value of type int[]. It’s a reference type, which means that it represents a reference to the data rather than the data itself. This is important when assigning one array value to another. For instance, we execute this code:

int[] b = a;

And now the memory looks like this:

image

Both values are pointing at the same underlying storage. We call this aliasing the array buffer. In Java, any number of values can point to same the underlying array storage. Because of this, the language has no idea how many pointers point to the storage at compile time; therefore, to determine when to clean up the storage, Java uses garbage collection. Periodically, the entire heap is scanned to determine whether any references to the array storage remain, and if there are none, the buffer is freed.

Now this model is simple and fast, but, since the arrays have a fixed size, the programmer can’t add new elements to them once they’re created. This is a very common thing to want, so Java provides another type, java.util.ArrayList, for this. As it turns out, the model used by Java’s ArrayList is essentially the same model that Python uses for all of its lists.

Let’s look at this model more closely. Consider this statement in Python:

a = [ 1, 2, 3, 4, 5 ]

Once this is executed, the memory looks like this:

image

As in Java, the cell highlighted in red (a) is the value that actually has the Python type list. We can see this if we assign a to b:

b = a

image

Obviously, the disadvantage of this model is that it requires two allocations instead of one. The advantage of this model is that new elements can be added to the end of the vector, and all outstanding references to the vector will see the new elements. Suppose that the vector had capacity 5 when initially created, so that no room exists to add new elements onto the end of the existing storage. Then when we execute the following line:

b.append(6)

The memory looks like this:

image

Here, Python has created a new and larger allocation for the storage, copied the existing elements over, and freed the old allocation (indicated in gray). Because a and b both point to the PyListObject allocation, which has not changed, they both see the new elements:

>>> a
[1, 2, 3, 4, 5, 6]
>>> b
[1, 2, 3, 4, 5, 6]

In summary, Python’s model sacrifices some efficiency at runtime because it requires both garbage collection and two allocations, but it gains flexibility by permitting both aliasing and append operations.

Turning our attention to the C++ STL, we find that it has a different model from both Python and Java: it sacrifices aliasing but retains the ability for vectors to grow. For instance, after this C++ STL code executes:

std::vector a;
a.push_back(1);
a.push_back(2);
a.push_back(3);
a.push_back(4);
a.push_back(5);

The memory looks like this:

image

As before, the red box indicates the value of type std::vector. It is stored directly on the stack. It is still fundamentally a reference type, just as vectors in Python and Java are; note that the underlying storage does not have the type std::vector<int> but instead has the type int[] (a plain old C array).

Like Python vectors, STL vectors can grow. After executing this line:

a.push_back(6);

The STL does this (assuming that there isn’t enough space to grow the vector in-place):

image

Just as the Python list did, the STL vector allocated new storage, copied the elements over, and deleted the old storage.

Unlike Java arrays, however, STL vectors do not support aliasing the contents of the vector (at least, not without some unsafe code). Instead, assignment of a value of type std::vector copies the contents of the vector. Consider this line:

std::vector b = a;

This code results in the following memory layout:

image

The entire contents of the vector were copied into a new allocation. This is, as you might expect, a quite expensive operation, and represents the downside of the C++ STL approach. However, the STL approach comes with significant upsides as well: no garbage collection (via tracing GC or reference counting) is required, there is one less allocation to manage, and the vectors are allowed to grow just as Python lists are.

This covers the three main vector representations in use by most languages. They’re fairly standard and representative; if I didn’t mention a language here, it’s likely that its implementation uses one of these three techniques. It’s important to note that none of these are right or wrong per se—they all have advantages and disadvantages. In a future post, I’ll explain the way Rust’s vector model allows the programmer to choose the model appropriate for the task at hand.

Typestate Is Dead, Long Live Typestate!

| Comments

One well-known fact about Rust is that the typestate system, which was one of the most unique aspects of the language early on, was dropped in Rust 0.4. The reason was that “in practice, it found little use” (courtesy of Wikipedia), which is fairly accurate. However, what’s less well known is that, in the meantime, Rust gained the building blocks necessary for typestate via its uniqueness typing system. With the right patterns, most of the safety guarantees that typestate enabled can be achieved, although it’s not as easy to use.

Let’s start with the simple example of a file that can be open or closed. We want to ensure at compile time that no methods that require the file to be open (for example, reading) can be called on the file while it is closed. With typestate, we would define the functions as follows:

use core::libc;

struct File {
    descriptor: int
}

pred is_open(file: File) -> bool {
    return file.descriptor >= 0;
}

fn open(path: &str) -> File : is_open {
    let file = File { descriptor: libc::open(path) };
    check is_open(file);
    return file;
}

fn close(file: &mut File) {
    libc::close(file.descriptor);
    file.descriptor = -1;
}

fn read(file: &File : is_open, buf: &mut [u8], len: uint) {
    libc::read(file.descriptor, ...)
}

And this is how this module might be used:

fn main() {
    let file: File : is_open = open("hello.txt");
    read(&file, ...);
    close(file);

    read(&file, ...);    // error: expected File : is_open but found File
    check is_open(file); // will fail at runtime
}

The constructs here that differ from Rust of today are:

  • Constraints are special type kinds that can be attached to types with the : syntax; e.g. File : is_open.

  • The pred keyword declares a predicate function, which defines both a function and a constraint.

  • All values have unconstrained types when initially constructed. To add a constraint to a type, we use the check keyword. The check expression evaluates a predicate and fails at runtime if the predicate returns false; otherwise, it adds the appropriate constraint to the type of the predicate’s argument.

Now let’s look at how we could achieve this in current Rust. We use the branding pattern:

struct File<State> {
    priv descriptor: int,
}

// Make the type noncopyable.
impl<T> File<T> : Drop {
    fn finalize(&self) {}
}

struct Open(@Open);
struct Closed(@Closed);

fn check_open<T>(file: File<T>) -> File<Open> {
    assert file.descriptor >= 0;
    let new_file: File<Open> = File {
        descriptor: file.descriptor
    };
    return new_file;
}

fn open(path: &str) -> File<Open> {
    let file: File<Closed> = File { descriptor: libc::open(path) };
    let file: File<Open> = check_open(file);
    return file;
}

fn close<T>(file: File<T>) -> File<Closed> {
    let new_file: File<Closed> = File {
        descriptor: -1
    };
    libc::close(file.descriptor);
    return new_file;
}

fn read(file: &File<Open>, buf: &mut [u8], len: uint) {
    libc::read(file.descriptor, ...)
}

Using this code has a different feel to it:

fn main() {
    let file: File<Open> = open("hello.txt");
    read(&file, ...);
    let file: File<Closed> = close(file);

    read(&file, ...);  // error: expected File<Open> but found File<Closed>
    let file: File<Open> = check_open(file); // will fail at runtime
}

The differences between this code and the original code using typestate are:

  • Rather than directly altering the constraints attached to a value’s type, the functions that change typestate take a value of one type and return a different value of a different type. For example, close() takes a value of File<T> for any state T and returns a value of type File<Closed>.

  • Instead of the built-in notion of a predicate, this code uses a phantom type. A phantom type is a type for which no values can be constructed—in this example, there is no way to construct a value of type Open or Closed. Instead, these types are solely used as “markers”. In the code above, a value of type File<Open> represents an open file, and a value of type File<Closed> represents a closed file. We call these branded types, because File is branded with the Open or Closed status. Generics (e.g. File<T>) can be used when the state of a file is irrelevant; e.g. if a function can operate on both closed or open files.

  • File instances are made noncopyable. This is important to prevent code like this from compiling:

    let file: File<Open> = open("hello.txt");
    let _: File<Closed> = close(file); // ignore the return value
    read(&file, ...);  // error: use of moved value `file`
    

The important idea is that to get a closed file, you must first surrender your open file. The uniqueness system in Rust allows the compiler to ensure this: when you change typestates, you must move your original value away, and the compiler will ensure that you can’t access it again.

  • The file descriptor field is made private to the containing module. This is important to disallow other modules from forging open or closed File instances. Otherwise, other code could simply convert an open file to a closed file the same way check_open does:

    let open_file: File<Open> = open("hello.txt");
    let closed_file: File<Closed> = close(open_file);
    let fake_open_file: File<Open> = File { descriptor: closed_file };
    // ^^^ error: use of private field 'descriptor'
    read(&fake_open_file, ...);
    

Since the File structure contains a private field, no code other than the containing module can create one. In this way, we ensure that nobody can forge instances of File and violate our invariants.

Now, it’s obvious that this isn’t perfect in terms of usability. For one, it’s a design pattern, and design patterns are the sincerest form of request for syntax. I’m not particularly concerned about this aspect, however, because syntactic sugar is readily achievable with macros.

The issue that I’m concerned with is deeper. One nice thing about typestate as previously implemented is that you don’t have to surrender your value; you can effectively “mutate” its type “in-place”. This saves you from writing temporary variables all over the place and also saves some (cheap) copies at runtime. For example, you can write:

let file = open("hello.txt");
read(&file, ...);
close(file);

Instead of:

let file = open("hello.txt");
read(&file, ...);
let file = close(file);

In Rust, however, this causes complications, which we never fully resolved. (In fact, this is part of what led to typestate’s removal.) Suppose that close mutated the type of its argument to change it from &File<Open> to &File<Closed>. Then consider the following code:

trait Foo {
    fn speak(&self);
}

impl File<Open> : Foo {
    fn speak(&self) {
        io::println("woof");
    }
}

trait Bar {
    fn speak(&self, x: int);
}

impl File<Closed> : Bar {
    fn speak(&self) {
        io::println("meow");
    }
}

let file = open("hello.txt");
for 5.times {
    file.speak();
    close(&file);
}

How do we compile this code? The first time around the for 5.times { ... } loop, file.speak() should resolve to Foo::speak; the second time around, file.speak() should resolve to Bar::speak. Needless to say, this makes compiling extremely difficult: we would have to consider the lexical scope of every single method invocation and compile it for each possible predicate!

Because of these and other complications, mutating the type doesn’t seem possible in the general case. We would certainly need to introduce some set of restrictions—perhaps we would need to formalize the notion of a “constraint” in the type system (probably by introducing a new type kind) and then introduce some restrictions on implementation declarations to prevent instances from depending on constraints. Whatever system we come up would be pretty complex and would require a fair bit of thought to get right.

So I’d like to try to play with the current setup and see how far we get with it. In future versions of the language (post-1.0), it might be worthwhile to try to allow some sort of in-place “mutation” of types, similar to languages with true typestate. Overall, though, the combination of uniqueness and branding places today’s Rust in an interesting position, supporting much of the power that came with typestate in a simple system.

Unique Pointers Aren’t Just About Memory Management

| Comments

One of the most unusual features of Rust, especially when compared to languages that aren’t C++, is the three types of pointers: borrowed pointers (&T), unique pointers (~T), and managed pointers (@T). Most people quite rightly ask “why three pointers? Isn’t one enough?” The usual answer is that unique pointers help with manual memory management:

  • Managed pointers (@T) allow convenient garbage collection.

  • Unique pointers (~T) work like malloc and free from C to allow programmers who don’t want the overhead and complexity of GC to avoid it.

  • Borrowed pointers (&T) allow functions to work equally well with both unique and managed pointers.

This is all true, but there’s another, equally important reason that’s often overlooked: unique pointers allow for efficient, safe concurrency.

To see why, let’s consider the possible ways that an actor- or CSP-based system could enforce safe message passing. By safe message passing I mean that actors can’t create data races by simultaneously accessing shared mutable data. In short, we want to enforce that this adage is followed (courtesy of Rob Pike)–“do not communicate by sharing memory; share memory by communicating.”

There are three simple ways to do this:

  1. Copy all messages sent from actor to actor. Changes that one actor makes to the contents of any message do not affect the other actors’ copies of the message.

  2. Require that all messages sent from actor to actor be immutable. No actor may make changes to any message after it’s created.

  3. Make messages inaccessible to the sender once sent–senders “give away” their messages. Only one actor may mutate a message at any given time.

Each of these patterns has advantages and disadvantages:

  1. Copying all messages has the advantage that it’s simple to reason about, and the programmer doesn’t have to worry about mutability restrictions. The disadvantage is that it comes with a significant performance cost, both in terms of allocation overhead and the copying itself.

  2. Requiring that messages be immutable has the advantage that many messages can be efficiently sent, but it still can lead to copying in many cases. Consider, for example, an application that spawns off a task to decode a large JPEG image. To be efficient, the image decoding algorithm generally wants to decode into a mutable buffer. But the decoded image data must be immutable to be sent, which necessitates a potentially-expensive copy of the pixel data out of the work buffer to an immutable location.

  3. Making messages inaccessible to the sender has the advantage that it’s simple and fast, but it has the disadvantage that it could lead to copying if both the sender and receiver need to access the memory after the send operation.

Because one pattern rarely fits every use case, most actor-based languages, including Rust, have varying levels of support for all three of these patterns (and for more complex patterns that don’t appear in the above list, such as software transactional memory). However, each language tends to favor one of the three patterns “by default”. For example, Erlang leans toward option #1 (copying all messages), Clojure leans toward option #2 (immutable sharing), while Rust leans toward option #3 (giving messages away). The important thing to note here is that all of the patterns have advantages and disadvantages, and so different scenarios will call for one or the other. Consider the image decoding example from before; pattern #3 is by far the most efficient way to handle this, as the buffer needs to be mutable while the image decoder works on it, but the decoder has no need for the image after decoding is done.

Now the simplest way to support pattern #3 safely–in other words, to enforce at compile time that only one actor can hold onto a message at any given time–is through unique pointers. The compiler guarantees that only one reference exists to a uniquely-owned object, enforcing the property we want. Unique pointers support a move operation, which allows functions to “give a pointer away” to another function. So by simply requiring that the “send” method takes a unique pointer and moves its argument, we teach the compiler everything it needs to know to enforce safe concurrency.

In this way, unique pointers aren’t just a tool for manual memory management. They’re also a powerful tool for eliminating data races at compile time. The fact that they also allow Rust programs to avoid the garbage collector is just an added bonus.

A Gentle Introduction to Traits in Rust

| Comments

Rust traits pack a lot of flexibility into a simple system, and they’re one of my favorite features of the language. But as a result of the rapid pace of the language’s development, there’s been a fair amount of confusion as to how they work. As such, I figured I’d write up a quick tutorial explaining why and how to use them.

This tutorial assumes only basic knowledge of C-like languages, so I’ll try to explain everything specific to Rust that might be unclear along the way. Also note that a couple of these features are unimplemented, so if you try this today the syntax will be a little different.

Simple implementations

In keeping with the theme of my previous blog posts on classes, let’s start by writing a game. I’ll start by defining a struct Monster and a struct Player like this:

struct Monster {
    name: &str;      // `&str` is a reference to a string
    mut health: int; // `mut` indicates that the health can be changed
}

struct Player {
    mut health: int;
}

Now I can create instances of each:

fn main() {  // `fn` defines a function
    let monster = Monster {
        name: "Gelatinous Cube",
        health: 50
    };
    let player = Player {
        health: 100
    };
}

Without some functionality, this isn’t a particularly interesting game. So let’s add a method to Monster:

impl Monster {
    fn attack(&self, player: &Player) {
        // fmt! is string formatting; this prints "Gelatinous Cube hits you!"
        io::println(fmt!("%s hits you!", self.name));
        player.health -= 10;
    }
}

And I can call it this way, inside main:

monster.attack(&player);

There are several things to note here.

  • References are explicit in Rust: the & sigil indicates that the method attack takes a reference to the player, not the player itself. If I didn’t write that, then the player would be copied into the method instead (and we’d get a compiler warning, because this indicates a bug).

  • I use the keyword impl to declare methods for a type. impl declarations can appear anywhere in the module that declared the type. The struct and impl pair appears a lot in Rust code; it nicely separates out data from implementation. Objective-C and C++ programmers will find this familiar.

  • Within an implementation, functions with a self parameter become methods. Python programmers will find this “explicit self” familiar. Because references are explicit in Rust, you specify how self is supposed to be passed; in this case, by reference (&self).

Generics

Now that we have basic implementations covered, let’s look at something completely different: generics. (We’ll come back to implementations later on.) Like many other languages, Rust features generic functions: functions that can operate on many different types. For example, here’s a function that returns true if a vector is empty:

// Vectors are written with square brackets around the type; e.g. a vector of
// ints is written `[int]`.
fn is_empty<T>(v: &[T]) -> bool {
    return v.len() == 0;
}

The generic type parameters are written inside the angle brackets (< and >), after the function name.

There’s nothing much more to say here; generics are pretty simple. In this form, however, they’re pretty limited, as we’ll see.

Limitations of generics

Let’s go back to our game example. Suppose I want to add functionality to save the state of the game to disk in JSON. I’ll implement some methods on Monster and Player to do this:

impl Monster {
    // `~str` means "a pointer to a string that'll be automatically freed"
    fn to_json(&self) -> ~str {
        return fmt!("{ name: \"%s\", health: %d }", self.name, self.health);
    }
}

impl Player {
    fn to_json(&self) -> ~str {
        return fmt!("{ health: %d }", self.health);
    }
}

Now imagine that I wanted a function to save any actor (either a monster or a player) into a file. Because monsters and players are different types, I need to use a generic function to handle both. My first attempt at the function looks like this:

fn save<T>(filename: &str, actor: &T) {
    // Because the writer returns an error code, I use .get() to mean "require
    // that this succeeded, and abort the program if it didn't".
    let writer = io::file_writer(filename, [ io::create, io::truncate ]).get();
    writer.write(actor.to_json());
    // Because of RAII, the file will automatically be closed.
}

Uh-oh. This doesn’t compile. I get the following error: “attempted access of field to_json on type &T, but no public field or method with that name was found”.

What the Rust compiler is telling me is that it doesn’t know that the type T in this function contains the method to_json. And, in fact, it might not. As written above, it’d be perfectly legal to call save on any type at all:

struct Penguin {
    name: &str;
}

save("penguin.txt", &Penguin { name: "Fred" });
// But how do I convert penguins to JSON?

So I’m stuck. But Rust provides a solution: traits.

Trait declaration

Traits are the way to tell the Rust compiler about functionality that a type must provide. They’re very similar in spirit to interfaces in Java, C#, and Go, and are similar in implementation to typeclasses in Haskell. They provide the solution to the problem I’m facing: I need to tell the Rust compiler, first of all, that some types can be converted to JSON, and, additionally, for the types that can be converted to JSON, how to do it.

To define a trait, I simply use the trait keyword:

trait ToJSON {
    fn to_json(&self) -> ~str;
}

This declares a trait named ToJSON, with one method that all types that implement the trait must define. That method is named to_json, and it takes its self parameter by reference.

Now I can define implementations of ToJSON for the various types I’m interested in. These implementations are exactly the same as above, except that we add : ToJSON.

impl Monster : ToJSON {
    // `~str` means "a pointer to a string that'll be automatically freed"
    fn to_json(&self) -> ~str {
        return fmt!("{ name: \"%s\", health: %d }", self.name, self.health);
    }
}

impl Player : ToJSON {
    fn to_json(&self) -> ~str {
        return fmt!("{ health: %d }", self.health);
    }
}

That’s all there is to it. Now I can modify the save function so that it does what I want.

Trait usage

Recall that the reason why the save function didn’t compile is that the Rust compiler didn’t know that the T type contained a to_json method. What I need is some way to tell the compiler that this function only accepts types that contain the methods I need to call. This is accomplished through trait restrictions. I modify the save function as follows:

fn save<T:ToJSON>(filename: &str, actor: &T) {
    let writer = io::file_writer(filename, [ io::create, io::truncate ]).get();
    writer.write(actor.to_json());
}

Note the addition of :ToJSON after the type parameter. This indicates that the function can only be called with types that implement the trait.

Now these calls to save will compile:

save("player.txt", &player);
save("monster.txt", &monster);

But this call will not:

save("penguin.txt", &Penguin { name: "Fred" });

I get the error “failed to find an implementation of trait ToJSON for Penguin”, just as expected.

Summing up

These are the basic features of traits and comprise most of what Rust programmers will need to know. There are only a few more features beyond these, which I’ll mention briefly:

  • Special traits. Some traits are known to the compiler and represent the built-in operations. Most notably, this includes the ubiquitous copy trait, which invokes the copy operation that occurs when you assign with let x = y. You’ll see T:copy in many generic functions for this reason. Other special traits include send, which is a trait that indicates the type is sendable, and add, sub, etc, which indicate the built-in arithmetic operators. The key is that, in all cases, traits simply specify what a generic type can do; when you want to do something with a type parameter like T, you specify a trait.

  • Generic traits. Traits can be generic, which is occasionally useful.

  • Default implementations. It’s often helpful for traits to provide default implementations of their methods that take over when the type doesn’t provide an implementation of its own. For example, the default implementation of to_json() might want to use the Rust reflection API to automatically create JSON for any type, even if that type doesn’t manually implement the to_json() method. (Note that this feature is currently being implemented.)

  • Trait composition. Sometimes we want one trait to include another trait. For example, the Num trait, which all number types in the language implement, obviously includes addition, subtraction, multiplication, etc. Trait composition allows traits to be “glued together” in this way. Note that this isn’t inheritance; it’s simply a convenience that allows trait methods to be combined together, like a mixin. (This is not fully implemented yet.)

  • First-class trait values. Rarely, it’s necessary to have a trait be a first-class value, like in Java or Go, instead of attached to a generic type parameter. This doesn’t come up particularly often, but Rust does support it in the rare cases in which it’s needed. Idiomatic Rust uses generics instead of Java-like interfaces.

That’s about all there is to traits. Traits are essentially Rust’s object system, but they’re simpler than many object systems and integrate especially well with generics.