Chapter 2. A Tour of Rust

Toute l’expérience d’un individu est construit sur le plan de son langage.
(An individual’s experience is built entirely in terms of his language.)

Henri Delacroix

In this chapter we’ll look at several short programs to see how Rust’s syntax, types, and semantics fit together to support safe, concurrent, and efficient code. We’ll walk through the process of downloading and installing Rust, show some simple mathematical code, try out a web server based on a third-party library, and use multiple threads to speed up the process of plotting the Mandelbrot set.

Downloading and Installing Rust

The best way to install Rust is to use rustup, the Rust installer. Go to https://rustup.rs and follow the instructions there.

You can, alternatively, go to https://www.rust-lang.org, click Downloads, and get pre-built packages for Linux, macOS, and Windows. Rust is also included in some operating system distributions. We prefer rustup because it’s a tool for managing Rust installations, like RVM for Ruby or NVM for Node. For example, when a new version of Rust is released, you’ll be able to upgrade with zero clicks by typing rustup update.

In any case, once you’ve completed the installation, you should have three new commands available at your command line:

$ cargo --version
cargo 0.18.0 (fe7b0cdcf 2017-04-24)
$ rustc --version
rustc 1.17.0 (56124baa9 2017-04-24)
$ rustdoc --version
rustdoc 1.17.0 (56124baa9 2017-04-24)
$

Here, the $ is the command prompt; on Windows, this would be C:> or something similar. In this transcript we run the three commands we installed, asking each to report which version it is. Taking each command in turn:

  • cargo is Rust’s compilation manager, package manager, and general-purpose tool. You can use Cargo to start a new project, build and run your program, and manage any external libraries your code depends on.

  • rustc is the Rust compiler. Usually we let Cargo invoke the compiler for us, but sometimes it’s useful to run it directly.

  • rustdoc is the Rust documentation tool. If you write documentation in comments of the appropriate form in your program’s source code, rustdoc can build nicely formatted HTML from them. Like rustc, we usually let Cargo run rustdoc for us.

As a convenience, Cargo can create a new Rust package for us, with some standard metadata arranged appropriately:

$ cargo new --bin hello
     Created binary (application) `hello` project

This command creates a new package directory named hello, and the --bin flag directs Cargo to prepare this as an executable, not a library. Looking inside the package’s top-level directory:

$ cd hello
$ ls -la
total 24
drwxrwxr-x.  4 jimb jimb 4096 Sep 22 21:09 .
drwx------. 62 jimb jimb 4096 Sep 22 21:09 ..
drwxrwxr-x.  6 jimb jimb 4096 Sep 22 21:09 .git
-rw-rw-r--.  1 jimb jimb    7 Sep 22 21:09 .gitignore
-rw-rw-r--.  1 jimb jimb   88 Sep 22 21:09 Cargo.toml
drwxrwxr-x.  2 jimb jimb 4096 Sep 22 21:09 src
$

We can see that Cargo has created a file Cargo.toml to hold metadata for the package. At the moment this file doesn’t contain much:

[package]
name = "hello"
version = "0.1.0"
authors = ["You <[email protected]>"]

[dependencies]

If our program ever acquires dependencies on other libraries, we can record them in this file, and Cargo will take care of downloading, building, and updating those libraries for us. We’ll cover the Cargo.toml file in detail in Chapter 8.

Cargo has set up our package for use with the git version control system, creating a .git metadata subdirectory, and a .gitignore file. You can tell Cargo to skip this step by specifying --vcs none on the command line.

The src subdirectory contains the actual Rust code:

$ cd src
$ ls -l
total 4
-rw-rw-r--. 1 jimb jimb 45 Sep 22 21:09 main.rs

It seems that Cargo has begun writing the program on our behalf. The main.rs file contains the text:

fn main() {
    println!("Hello, world!");
}

In Rust, you don’t even need to write your own “Hello, World!” program. And this is the extent of the boilerplate for a new Rust program: two files, totaling nine lines.

We can invoke the cargo run command from any directory in the package to build and run our program:

$ cargo run
   Compiling hello v0.1.0 (file:///home/jimb/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
     Running `/home/jimb/rust/hello/target/debug/hello`
Hello, world!
$

Here, Cargo has invoked the Rust compiler, rustc, and then run the executable it produced. Cargo places the executable in the target subdirectory at the top of the package:

$ ls -l ../target/debug
total 580
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 build
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 deps
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 examples
-rwxrwxr-x. 1 jimb jimb 576632 Sep 22 21:37 hello
-rw-rw-r--. 1 jimb jimb    198 Sep 22 21:37 hello.d
drwxrwxr-x. 2 jimb jimb     68 Sep 22 21:37 incremental
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 native
$ ../target/debug/hello
Hello, world!
$

When we’re through, Cargo can clean up the generated files for us:

$ cargo clean
$ ../target/debug/hello
bash: ../target/debug/hello: No such file or directory
$

A Simple Function

Rust’s syntax is deliberately unoriginal. If you are familiar with C, C++, Java, or JavaScript, you can probably find your way through the general structure of a Rust program. Here is a function that computes the greatest common divisor of two integers, using Euclid’s algorithm:

fn gcd(mut n: u64, mut m: u64) -> u64 {
    assert!(n != 0 && m != 0);
    while m != 0 {
        if m < n {
            let t = m;
            m = n;
            n = t;
        }
        m = m % n;
    }
    n
}

The fn keyword (pronounced “fun”) introduces a function. Here, we’re defining a function named gcd, which takes two parameters n and m, each of which is of type u64, an unsigned 64-bit integer. The -> token precedes the return type: our function returns a u64 value. Four-space indentation is standard Rust style.

Rust’s machine integer type names reflect their size and signedness: i32 is a signed 32-bit integer; u8 is an unsigned eight-bit integer (used for “byte” values), and so on. The isize and usize types hold pointer-sized signed and unsigned integers, 32 bits long on 32-bit platforms, and 64 bits long on 64-bit platforms. Rust also has two floating-point types, f32 and f64, which are the IEEE single- and double-precision floating-point types, like float and double in C and C++.

By default, once a variable is initialized, its value can’t be changed, but placing the mut keyword (pronounced “mute”, short for mutable) before the parameters n and m allows our function body to assign to them. In practice, most variables don’t get assigned to; the mut keyword on those that do can be a helpful hint when reading code.

The function’s body starts with a call to the assert! macro, verifying that neither argument is zero. The ! character marks this as a macro invocation, not a function call. Like the assert macro in C and C++, Rust’s assert! checks that its argument is true, and if it is not, terminates the program with a helpful message including the source location of the failing check; this kind of abrupt termination is called a panic. Unlike C and C++, in which assertions can be skipped, Rust always checks assertions regardless of how the program was compiled. There is also a debug_assert! macro, whose assertions are skipped when the program is compiled for speed.

The heart of our function is a while loop containing an if statement and an assignment. Unlike C and C++, Rust does not require parentheses around the conditional expressions, but it does require curly braces around the statements they control.

A let statement declares a local variable, like t in our function. We don’t need to write out t’s type, as long as Rust can infer it from how the variable is used. In our function, the only type that works for t is u64, matching m and n. Rust only infers types within function bodies: you must write out the types of function parameters and return values, as we did before. If we wanted to spell out t’s type, we could write:

let t: u64 = m;

Rust has a return statement, but the gcd function doesn’t need one. If a function body ends with an expression that is not followed by a semicolon, that’s the function’s return value. In fact, any block surrounded by curly braces can function as an expression. For example, this is an expression that prints a message and then yields x.cos() as its value:

{
    println!("evaluating cos x");
    x.cos()
}

It’s typical in Rust to use this form to establish the function’s value when control “falls off the end” of the function, and use return statements only for explicit early returns from the midst of a function.

Writing and Running Unit Tests

Rust has simple support for testing built into the language. To test our gcd function, we can write:

#[test]
fn test_gcd() {
    assert_eq!(gcd(14, 15), 1);

    assert_eq!(gcd(2 * 3 * 5 * 11 * 17,
                   3 * 7 * 11 * 13 * 19),
               3 * 11);
}

Here we define a function named test_gcd, which calls gcd and checks that it returns correct values. The #[test] atop the definition marks test_gcd as a test function, to be skipped in normal compilations, but included and called automatically if we run our program with the cargo test command. Let’s assume we’ve edited our gcd and test_gcd definitions into the hello package we created at the beginning of the chapter. If our current directory is somewhere within the package’s subtree, we can run the tests as follows:

$ cargo test
   Compiling hello v0.1.0 (file:///home/jimb/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs
     Running /home/jimb/rust/hello/target/debug/deps/hello-2375a82d9e9673d7

running 1 test
test test_gcd ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

$

We can have test functions scattered throughout our source tree, placed next to the code they exercise, and cargo test will automatically gather them up and run them all.

The #[test] marker is an example of an attribute. Attributes are an open-ended system for marking functions and other declarations with extra information, like attributes in C++ and C#, or annotations in Java. They’re used to control compiler warnings and code style checks, include code conditionally (like #ifdef in C and C++), tell Rust how to interact with code written in other languages, and so on. We’ll see more examples of attributes as we go.

Handling Command-Line Arguments

If we want our program to take a series of numbers as command-line arguments and print their greatest common divisor, we can replace the main function with the following:

use std::io::Write;
use std::str::FromStr;

fn main() {
    let mut numbers = Vec::new();

    for arg in std::env::args().skip(1) {
        numbers.push(u64::from_str(&arg)
                     .expect("error parsing argument"));
    }

    if numbers.len() == 0 {
        writeln!(std::io::stderr(), "Usage: gcd NUMBER ...").unwrap();
        std::process::exit(1);
    }

    let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }

    println!("The greatest common divisor of {:?} is {}",
             numbers, d);
}

This is a large block of code, so let’s take it piece by piece:

use std::io::Write;
use std::str::FromStr;

The use declarations bring the two traits Write and FromStr into scope. We’ll cover traits in detail in Chapter 11, but for now we’ll simply say that a trait is a collection of methods that types can implement. Although we never use the names Write or FromStr elsewhere in the program, a trait must be in scope in order to use its methods. In the present case:

  • Any type that implements the Write trait has a write_fmt method that writes formatted text to a stream. The std::io::Stderr type implements Write, and we’ll use the writeln! macro to print error messages; that macro expands to code that uses the write_fmt method.

  • Any type that implements the FromStr trait has a from_str method that tries to parse a value of that type from a string. The u64 type implements FromStr, and we’ll call u64::from_str to parse our command-line arguments.

Moving on to the program’s main function:

fn main() {

Our main function doesn’t return a value, so we can simply omit the -> and type that would normally follow the parameter list.

let mut numbers = Vec::new();

We declare a mutable local variable numbers, and initialize it to an empty vector. Vec is Rust’s growable vector type, analogous to C++’s std::vector, a Python list, or a JavaScript array. Even though vectors are designed to be grown and shrunk dynamically, we must still mark the variable mut for Rust to let us push numbers onto the end of it.

The type of numbers is Vec<u64>, a vector of u64 values, but as before, we don’t need to write that out. Rust will infer it for us, in part because what we push onto the vector are u64 values, but also because we pass the vector’s elements to gcd, which accepts only u64 values.

for arg in std::env::args().skip(1) {

Here we use a for loop to process our command-line arguments, setting the variable arg to each argument in turn, and evaluating the loop body.

The std::env::args function returns an iterator, a value that produces each argument on demand, and indicates when we’re done. Iterators are ubiquitous in Rust; the standard library includes other iterators that produce the elements of a vector, the lines of a file, messages received on a communications channel, and almost anything else that makes sense to loop over. Rust’s iterators are very efficient: the compiler is usually able to translate them into the same code as a handwritten loop. We’ll show how this works and give examples in Chapter 15.

Beyond their use with for loops, iterators include a broad selection of methods you can use directly. For example, the first value produced by the iterator returned by std::env::args is always the name of the program being run. We want to skip that, so we call the iterator’s skip method to produce a new iterator that omits that first value.

numbers.push(u64::from_str(&arg)
             .expect("error parsing argument"));

Here we call u64::from_str to attempt to parse our command-line argument arg as an unsigned 64-bit integer. Rather than a method we’re invoking on some u64 value we have at hand, u64::from_str is a function associated with the u64 type, akin to a static method in C++ or Java. The from_str function doesn’t return a u64 directly, but rather a Result value that indicates whether the parse succeeded or failed. A Result value is one of two variants:

  • A value written Ok(v), indicating that the parse succeeded and v is the value produced

  • A value written Err(e), indicating that the parse failed and e is an error value explaining why

Functions that perform input or output or otherwise interact with the operating system all return Result types whose Ok variants carry successful results—the count of bytes transferred, the file opened, and so on—and whose Err variants carry an error code from the system. Unlike most modern languages, Rust does not have exceptions: all errors are handled using either Result or panic, as outlined in Chapter 7.

We check the success of our parse by using Result’s expect method. If the result is some Err(e), expect prints a message that includes a description of e, and exits the program immediately. However, if the result is Ok(v), expect simply returns v itself, which we are finally able to push onto the end of our vector of numbers.

if numbers.len() == 0 {
    writeln!(std::io::stderr(), "Usage: gcd NUMBER ...").unwrap();
    std::process::exit(1);
}

There’s no greatest common divisor of an empty set of numbers, so we check that our vector has at least one element, and exit the program with an error if it doesn’t. We use the writeln! macro to write our error message to the standard error output stream, provided by std::io::stderr(). The .unwrap() call is a terse way to check that the attempt to print the error message did not itself fail; an expect call would work too, but that’s probably not worth it.

let mut d = numbers[0];
for m in &numbers[1..] {
    d = gcd(d, *m);
}

This loop uses d as its running value, updating it to stay the greatest common divisor of all the numbers we’ve processed so far. As before, we must mark d as mutable, so that we can assign to it in the loop.

The for loop has two surprising bits to it. First, we wrote for m in &numbers[1..]; what is the & operator for? Second, we wrote gcd(d, *m); what is the * in *m for? These two details are complementary to each other.

Up to this point, our code has operated only on simple values like integers that fit in fixed-size blocks of memory. But now we’re about to iterate over a vector, which could be of any size whatsoever—possibly very large. Rust is cautious when handling such values: it wants to leave the programmer in control over memory consumption, making it clear how long each value lives, while still ensuring memory is freed promptly when no longer needed.

So when we iterate, we want to tell Rust that ownership of the vector should remain with numbers; we are merely borrowing its elements for the loop. The & operator in &numbers[1..] borrows a reference to the vector’s elements from the second onward. The for loop iterates over the referenced elements, letting m borrow each element in succession. The * operator in *m dereferences m, yielding the value it refers to; this is the next u64 we want to pass to gcd. Finally, since numbers owns the vector, Rust automatically frees it when numbers goes out of scope at the end of main.

Rust’s rules for ownership and references are key to Rust’s memory management and safe concurrency; we discuss them in detail in Chapter 4 and its companion, Chapter 5. You’ll need to be comfortable with those rules to be comfortable in Rust, but for this introductory tour, all you need to know is that &x borrows a reference to x, and that *r is the value that the reference r refers to.

Continuing our walk through the program:

println!("The greatest common divisor of {:?} is {}",
         numbers, d);

Having iterated over the elements of numbers, the program prints the results to the standard output stream. The println! macro takes a template string, substitutes formatted versions of the remaining arguments for the {...} forms as they appear in the template string, and writes the result to the standard output stream.

Unlike C and C++, which require main to return zero if the program finished successfully, or a nonzero exit status if something went wrong, Rust assumes that if main returns at all, the program finished successfully. Only by explicitly calling functions like expect or std::process::exit can we cause the program to terminate with an error status code.

The cargo run command allows us to pass arguments to our program, so we can try out our command-line handling:

$ cargo run 42 56
   Compiling hello v0.1.0 (file:///home/jimb/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs
     Running `/home/jimb/rust/hello/target/debug/hello 42 56`
The greatest common divisor of [42, 56] is 14
$ cargo run 799459 28823 27347
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `/home/jimb/rust/hello/target/debug/hello 799459 28823 27347`
The greatest common divisor of [799459, 28823, 27347] is 41
$ cargo run 83
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `/home/jimb/rust/hello/target/debug/hello 83`
The greatest common divisor of [83] is 83
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `/home/jimb/rust/hello/target/debug/hello`
Usage: gcd NUMBER ...
$

We’ve used a few features from Rust’s standard library in this section. If you’re curious about what else is available, we strongly encourage you to try out Rust’s online documentation. It has a live search feature that makes exploration easy, and even includes links to the source code. The rustup command automatically installs a copy on your computer when you install Rust itself. You can view the standard library documentation in your browser with the command:

$ rustup doc --std

You can also view it on the web at https://doc.rust-lang.org/.

A Simple Web Server

One of Rust’s strengths is the freely available collection of library packages published on the website crates.io. The cargo command makes it easy for our own code to use a crates.io package: it will download the right version of the package, build it, and update it as requested. A Rust package, whether a library or an executable, is called a crate; Cargo and crates.io both derive their names from this term.

To show how this works, we’ll put together a simple web server using the iron web framework, the hyper HTTP server, and various other crates on which they depend. As shown in Figure 2-1, our website will prompt the user for two numbers, and compute their greatest common divisor.

Web page offering to compute GCD
Figure 2-1. Web page offering to compute GCD

First, we’ll have Cargo create a new package for us, named iron-gcd:

$ cargo new --bin iron-gcd
     Created binary (application) `iron-gcd` project
$ cd iron-gcd
$

Then, we’ll edit our new project’s Cargo.toml file to list the packages we want to use; its contents should be as follows:

[package]
name = "iron-gcd"
version = "0.1.0"
authors = ["You <[email protected]>"]

[dependencies]
iron = "0.5.1"
mime = "0.2.3"
router = "0.5.1"
urlencoded = "0.5.0"

Each line in the [dependencies] section of Cargo.toml gives the name of a crate on crates.io, and the version of that crate we would like to use. There may well be versions of these crates on crates.io newer than those shown here, but by naming the specific versions we tested this code against, we can ensure the code will continue to compile even as new versions of the packages are published. We’ll discuss version management in more detail in Chapter 8.

Note that we need only name those packages we’ll use directly; cargo takes care of bringing in whatever other packages those need in turn.

For our first iteration, we’ll keep the web server simple: it will serve only the page that prompts the user for numbers to compute with. In iron-gcd/src/main.rs, we’ll place the following text:

extern crate iron;
#[macro_use] extern crate mime;

use iron::prelude::*;
use iron::status;

fn main() {
    println!("Serving on http://localhost:3000...");
    Iron::new(get_form).http("localhost:3000").unwrap();
}

fn get_form(_request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(r#"
        <title>GCD Calculator</title>
        <form action="/gcd" method="post">
          <input type="text" name="n"/>
          <input type="text" name="n"/>
          <button type="submit">Compute GCD</button>
        </form>
    "#);

    Ok(response)
}

We start with two extern crate directives, which make the iron and mime crates that we cited in our Cargo.toml file available to our program. The #[macro_use] attribute before the extern crate mime item alerts Rust that we plan to use macros exported by this crate.

Next, we have use declarations to bring in some of those crates’ public features. The declaration use iron::prelude::* makes all the public names of the iron::prelude module directly visible in our own code. Generally, it’s preferable to spell out the name you wish to use, as we did for iron::status; but by convention, when a module is named prelude, that means that its exports are intended to provide the sort of general facilities that any user of the crate will probably need. So in this case, a wildcard use directive makes a bit more sense.

Our main function is simple: it prints a message reminding us how to connect to our server, calls Iron::new to create a server, and then sets it listening on TCP port 3000 on the local machine. We pass the get_form function to Iron::new, indicating that the server should use that function to handle all requests; we’ll refine this shortly.

The get_form function itself takes a mutable reference, written &mut, to a Request value representing the HTTP request we’ve been called to handle. While this particular handler function never uses its _request parameter, we’ll see one later that does. For the time being, giving the parameter a name beginning with _ tells Rust that we expect the variable to be unused, so it shouldn’t warn us about it.

In the body of the function, we build a Response value. The set_mut method uses its argument’s type to decide which part of the response to set, so each call to set_mut is actually setting a different part of response: passing status::Ok sets the HTTP status; passing the media type of the content (using the handy mime! macro that we imported from the mime crate) sets the Content-Type header; and passing a string sets the response body.

Since the response text contains a lot of double quotes, we write it using the Rust “raw string” syntax: the letter r, zero or more hash marks (that is, the # character), a double quote, and then the contents of the string, terminated by another double quote followed by the same number of hash marks. Any character may occur within a raw string without being escaped, including double quotes; in fact, no escape sequences like " are recognized. We can always ensure the string ends where we intend by using more hash marks around the quotes than ever appear in the text.

Our function’s return type, IronResult<Response>, is another variant of the Result type we encountered earlier: this is either Ok(r) for some successful Response value r, or Err(e) for some error value e. We construct our return value Ok(response) at the bottom of the function body, using the “last expression” syntax to implicitly specify the function’s return value.

Having written main.rs, we can use the cargo run command to do everything needed to set it running: fetching the needed crates, compiling them, building our own program, linking everything together, and starting it up:

$ cargo run
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading iron v0.5.1
 Downloading urlencoded v0.5.0
 Downloading router v0.5.1
 Downloading hyper v0.10.8
 Downloading lazy_static v0.2.8
 Downloading bodyparser v0.5.0
...
   Compiling conduit-mime-types v0.7.3
   Compiling iron v0.5.1
   Compiling router v0.5.1
   Compiling persistent v0.3.0
   Compiling bodyparser v0.5.0
   Compiling urlencoded v0.5.0
   Compiling iron-gcd v0.1.0 (file:///home/jimb/rust/iron-gcd)
     Running `target/debug/iron-gcd`
Serving on http://localhost:3000...

At this point, we can visit the given URL in our browser and see the page shown earlier in Figure 2-1.

Unfortunately, clicking Compute GCD doesn’t do anything, other than navigate our browser to the URL http://localhost:3000/gcd, which then shows the same page; in fact, every URL on our server does this. Let’s fix that next, using the Router type to associate different handlers with different paths.

First, let’s arrange to be able to use Router without qualification, by adding the following declarations to iron-gcd/src/main.rs:

extern crate router;
use router::Router;

Rust programmers typically gather all their extern crate and use declarations together toward the top of the file, but this isn’t strictly necessary: Rust allows declarations to occur in any order, as long as they appear at the appropriate level of nesting. (Macro definitions and extern crate items with #[macro_use] attributes are exceptions to this rule: they must appear before they are used.)

We can then modify our main function to read as follows:

fn main() {
    let mut router = Router::new();

    router.get("/", get_form, "root");
    router.post("/gcd", post_gcd, "gcd");

    println!("Serving on http://localhost:3000...");
    Iron::new(router).http("localhost:3000").unwrap();
}

We create a Router, establish handler functions for two specific paths, and then pass this Router as the request handler to Iron::new, yielding a web server that consults the URL path to decide which handler function to call.

Now we are ready to write our post_gcd function:

extern crate urlencoded;

use std::str::FromStr;
use urlencoded::UrlEncodedBody;

fn post_gcd(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    let form_data = match request.get_ref::<UrlEncodedBody>() {
        Err(e) => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("Error parsing form data: {:?}
", e));
            return Ok(response);
        }
        Ok(map) => map
    };

    let unparsed_numbers = match form_data.get("n") {
        None => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("form data has no 'n' parameter
"));
            return Ok(response);
        }
        Some(nums) => nums
    };

    let mut numbers = Vec::new();
    for unparsed in unparsed_numbers {
        match u64::from_str(&unparsed) {
            Err(_) => {
                response.set_mut(status::BadRequest);
                response.set_mut(
                    format!("Value for 'n' parameter not a number: {:?}
",
                            unparsed));
                return Ok(response);
            }
            Ok(n) => { numbers.push(n); }
        }
    }

    let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(
        format!("The greatest common divisor of the numbers {:?} is <b>{}</b>
",
                numbers, d));
    Ok(response)
}

The bulk of this function is a series of match expressions, which will be unfamiliar to C, C++, Java, and JavaScript programmers, but a welcome sight to those who work with Haskell and OCaml. We’ve mentioned that a Result is either a value Ok(s) for some success value s, or Err(e) for some error value e. Given some Result res, we can check which variant it is and access whichever value it holds with a match expression of the form:

match res {
    Ok(success) => { ... },
    Err(error)  => { ... }
}

This is a conditional, like an if statement or a switch statement in C: if res is Ok(v), then it runs the first branch, with the variable success set to v. Similarly, if res is Err(e), it runs the second branch with error set to e. The success and error variables are each local to their branch. The value of the entire match expression is the value of the branch that runs.

The beauty of a match expression is that the program can only access the value of a Result by first checking which variant it is; one can never misinterpret a failure value as a successful completion. Whereas in C and C++ it’s a common error to forget to check for an error code or a null pointer, in Rust, these mistakes are caught at compile time. This simple measure is a significant advance in usability.

Rust allows you to define your own types like Result with value-carrying variants, and use match expressions to analyze them. Rust calls these types enums; you may know them from other languages as algebraic data types. We describe enumerations in detail in Chapter 10.

Now that you can read match expressions, the structure of post_gcd should be clear:

  • It calls request.get_ref::<UrlEncodedBody>() to parse the request’s body as a table mapping query parameter names to arrays of values; if this parse fails, it reports the error back to the client. The ::<UrlEncodedBody> part of the method call is a type parameter indicating which part of the Request get_ref should retrieve. In this case, the UrlEncodedBody type refers to the body, parsed as a URL-encoded query string. We’ll talk more about type parameters in the next section.

  • Within that table, it finds the value of the parameter named "n", which is where the HTML form places the numbers entered into the web page. This value will be not a single string but a vector of strings, as query parameter names can be repeated.

  • It walks the vector of strings, parsing each one as an unsigned 64-bit number, and returning an appropriate failure page if any of the strings fail to parse.

  • Finally, it computes the numbers’ greatest common divisor as before, and constructs a response describing the results. The format! macro uses the same kind of string template as the writeln! and println! macros, but returns a string value, rather than writing the text to a stream.

The last remaining piece is the gcd function we wrote earlier. With that in place, you can interrupt any servers you might have left running, and rebuild and restart the program:

$ cargo run
   Compiling iron-gcd v0.1.0 (file:///home/jimb/rust/iron-gcd)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/iron-gcd`
Serving on http://localhost:3000...

This time, by visiting http://localhost:3000, entering some numbers, and clicking the Compute GCD button, you should actually see some results (Figure 2-2).

Web page showing results of computing GCD
Figure 2-2. Web page showing results of computing GCD

Concurrency

One of Rust’s great strengths is its support for concurrent programming. The same rules that ensure Rust programs are free of memory errors also ensure threads can share memory only in ways that avoid data races. For example:

  • If you use a mutex to coordinate threads making changes to a shared data structure, Rust ensures that you can’t access the data except when you’re holding the lock, and releases the lock automatically when you’re done. In C and C++, the relationship between a mutex and the data it protects is left to the comments.

  • If you want to share read-only data among several threads, Rust ensures that you cannot modify the data accidentally. In C and C++, the type system can help with this, but it’s easy to get it wrong.

  • If you transfer ownership of a data structure from one thread to another, Rust makes sure you have indeed relinquished all access to it. In C and C++, it’s up to you to check that nothing on the sending thread will ever touch the data again. If you don’t get it right, the effects can depend on what happens to be in the processor’s cache and how many writes to memory you’ve done recently. Not that we’re bitter.

In this section, we’ll walk you through the process of writing your second multi-threaded program.

Although you probably weren’t aware of it, you’ve already written your first: the Iron web framework you used to implement the Greatest Common Divisor server uses a pool of threads to run request handler functions. If the server receives simultaneous requests, it may run the get_form and post_gcd functions in several threads at once. That may come as a bit of a shock, since we certainly didn’t have concurrency in mind when we wrote those functions. But Rust guarantees this is safe to do, no matter how elaborate your server gets: if your program compiles, it is free of data races. All Rust functions are thread-safe.

This section’s program plots the Mandelbrot set, a fractal produced by iterating a simple function on complex numbers. Plotting the Mandelbrot set is often called an embarrassingly parallel algorithm, because the pattern of communication between the threads is so simple; we’ll cover more complex patterns in Chapter 19, but this task demonstrates some of the essentials.

To get started, we’ll create a fresh Rust project:

$ cargo new --bin mandelbrot
     Created binary (application) `mandelbrot` project

All the code will go in mandelbrot/src/main.rs, and we’ll add some dependencies to mandelbrot/Cargo.toml.

Before we get into the concurrent Mandelbrot implementation, we need to describe the computation we’re going to perform.

What the Mandelbrot Set Actually Is

When reading code, it’s helpful to have a concrete idea of what it’s trying to do, so let’s take a short excursion into some pure mathematics. We’ll start with a simple case, and then add complicating details until we arrive at the calculation at the heart of the Mandelbrot set.

Here’s an infinite loop, written using Rust’s dedicated syntax for that, a loop statement:

fn square_loop(mut x: f64) {
    loop {
        x = x * x;
    }
}

In real life, Rust can see that x is never used for anything, and so might not bother computing its value. But for the time being, assume the code runs as written. What happens to the value of x? Squaring any number smaller than 1 makes it smaller, so it approaches zero; squaring 1 yields 1; squaring a number larger than 1 makes it larger, so it approaches infinity; and squaring a negative number makes it positive, after which it behaves as one of the prior cases (Figure 2-3).

real number line, showing effect of repeated squaring on various numbers
Figure 2-3. Effects of repeatedly squaring a number

So depending on the value you pass to square_loop, x either approaches zero, stays at 1, or approaches infinity.

Now consider a slightly different loop:

fn square_add_loop(c: f64) {
    let mut x = 0.;
    loop {
        x = x * x + c;
    }
}

This time, x starts at zero, and we tweak its progress in each iteration by adding in c after squaring it. This makes it harder to see how x fares, but some experimentation shows that if c is greater than 0.25, or less than –2.0, then x eventually becomes infinitely large; otherwise, it stays somewhere in the neighborhood of zero.

The next wrinkle: instead of using f64 values, consider the same loop using complex numbers. The num crate on crates.io provides a complex number type we can use, so we must add a line for num to the [dependencies] section in our program’s Cargo.toml file. Here’s the entire file, up to this point (we’ll be adding more later):

[package]
name = "mandelbrot"
version = "0.1.0"
authors = ["You <[email protected]>"]

[dependencies]
num = "0.1.27"

Now we can write the penultimate version of our loop:

extern crate num;
use num::Complex;

#[allow(dead_code)]
fn complex_square_add_loop(c: Complex<f64>) {
    let mut z = Complex { re: 0.0, im: 0.0 };
    loop {
        z = z * z + c;
    }
}

It’s traditional to use z for complex numbers, so we’ve renamed our looping variable. The expression Complex { re: 0.0, im: 0.0 } is the way we write complex zero using the num crate’s Complex type. Complex is a Rust structure type (or struct), defined like this:

struct Complex<T> {
    /// Real portion of the complex number
    re: T,

    /// Imaginary portion of the complex number
    im: T
}

The preceding code defines a struct named Complex, with two fields, re and im. Complex is a generic structure: you can read the <T> after the type name as “for any type T”. For example, Complex<f64> is a complex number whose re and im fields are f64 values, Complex<f32> would use 32-bit floats, and so on. Given this definition, an expression like Complex { re: R, im: I } produces a Complex value with its re field initialized to R, and its im field initialized to I.

The num crate arranges for *, +, and other arithmetic operators to work on Complex values, so the rest of the function works just like the prior version, except that it operates on points on the complex plane, not just points along the real number line. We’ll explain how you can make Rust’s operators work with your own types in Chapter 12.

Finally, we’ve reached the destination of our pure math excursion. The Mandelbrot set is defined as the set of complex numbers c for which z does not fly out to infinity. Our original simple squaring loop was predictable enough: any number greater than 1 or less than –1 flies away. Throwing a + c into each iteration makes the behavior a little harder to anticipate: as we said earlier, values of c greater than 0.25 or less than –2 cause z to fly away. But expanding the game to complex numbers produces truly bizarre and beautiful patterns, which are what we want to plot.

Since a complex number c has both real and imaginary components c.re and c.im, we’ll treat these as the x and y coordinates of a point on the Cartesian plane, and color the point black if c is in the Mandelbrot set, or a lighter color otherwise. So for each pixel in our image, we must run the preceding loop on the corresponding point on the complex plane, see whether it escapes to infinity or orbits around the origin forever, and color it accordingly.

The infinite loop takes a while to run, but there are two tricks for the impatient. First, if we give up on running the loop forever and just try some limited number of iterations, it turns out that we still get a decent approximation of the set. How many iterations we need depends on how precisely we want to plot the boundary. Second, it’s been shown that, if z ever once leaves the circle of radius two centered at the origin, it will definitely fly infinitely far away from the origin eventually.

So here’s the final version of our loop, and the heart of our program:

extern crate num;
use num::Complex;

/// Try to determine if `c` is in the Mandelbrot set, using at most `limit`
/// iterations to decide.
///
/// If `c` is not a member, return `Some(i)`, where `i` is the number of
/// iterations it took for `c` to leave the circle of radius two centered on the
/// origin. If `c` seems to be a member (more precisely, if we reached the
/// iteration limit without being able to prove that `c` is not a member),
/// return `None`.
fn escape_time(c: Complex<f64>, limit: u32) -> Option<u32> {
    let mut z = Complex { re: 0.0, im: 0.0 };
    for i in 0..limit {
        z = z * z + c;
        if z.norm_sqr() > 4.0 {
            return Some(i);
        }
    }

    None
}

This function takes the complex number c that we want to test for membership in the Mandelbrot set, and a limit on the number of iterations to try before giving up and declaring c to probably be a member.

The function’s return value is an Option<u32>. Rust’s standard library defines the Option type as follows:

enum Option<T> {
    None,
    Some(T),
}

Option is an enumerated type, often called an enum, because its definition enumerates several variants that a value of this type could be: for any type T, a value of type Option<T> is either Some(v), where v is a value of type T; or None, indicating no T value is available. Like the Complex type we discussed earlier, Option is a generic type: you can use Option<T> to represent an optional value of any type T you like.

In our case, escape_time returns an Option<u32> to indicate whether c is in the Mandelbrot set—and if it’s not, how long we had to iterate to find that out. If c is not in the set, escape_time returns Some(i), where i is the number of the iteration at which z left the circle of radius two. Otherwise, c is apparently in the set, and escape_time returns None.

for i in 0..limit {

The earlier examples showed for loops iterating over command-line arguments and vector elements; this for loop simply iterates over the range of integers starting with 0 and up to (but not including) limit.

The z.norm_sqr() method call returns the square of z’s distance from the origin. To decide whether z has left the circle of radius two, instead of computing a square root, we just compare the squared distance with 4.0, which is faster.

You may have noticed that we use /// to mark the comment lines above the function definition; the comments above the members of the Complex structure start with /// as well. These are documentation comments; the rustdoc utility knows how to parse them, together with the code they describe, and produce online documentation. The documentation for Rust’s standard library is written in this form. We describe documentation comments in detail in Chapter 8.

The rest of the program is concerned with deciding which portion of the set to plot at what resolution, and distributing the work across several threads to speed up the calculation.

Parsing Pair Command-Line Arguments

The program needs to take several command-line arguments controlling the resolution of the image we’ll write, and the portion of the Mandelbrot set the image shows. Since these command-line arguments all follow a common form, here’s a function to parse them:

use std::str::FromStr;

/// Parse the string `s` as a coordinate pair, like `"400x600"` or `"1.0,0.5"`.
///
/// Specifically, `s` should have the form <left><sep><right>, where <sep> is
/// the character given by the `separator` argument, and <left> and <right> are both
/// strings that can be parsed by `T::from_str`.
///
/// If `s` has the proper form, return `Some<(x, y)>`. If it doesn't parse
/// correctly, return `None`.
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
    match s.find(separator) {
        None => None,
        Some(index) => {
            match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
                (Ok(l), Ok(r)) => Some((l, r)),
                _ => None
            }
        }
    }
}

#[test]
fn test_parse_pair() {
    assert_eq!(parse_pair::<i32>("",        ','), None);
    assert_eq!(parse_pair::<i32>("10,",     ','), None);
    assert_eq!(parse_pair::<i32>(",10",     ','), None);
    assert_eq!(parse_pair::<i32>("10,20",   ','), Some((10, 20)));
    assert_eq!(parse_pair::<i32>("10,20xy", ','), None);
    assert_eq!(parse_pair::<f64>("0.5x",    'x'), None);
    assert_eq!(parse_pair::<f64>("0.5x1.5", 'x'), Some((0.5, 1.5)));
}

The definition of parse_pair is a generic function:

fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {

You can read the clause <T: FromStr> aloud as, “For any type T that implements the FromStr trait...”. This effectively lets us define an entire family of functions at once: parse_pair::<i32> is a function that parses pairs of i32 values; parse_pair::<f64> parses pairs of floating-point values; and so on. This is very much like a function template in C++. A Rust programmer would call T a type parameter of parse_pair. When you use a generic function, Rust will often be able to infer type parameters for you, and you won’t need to write them out as we did in the test code.

Our return type is Option<(T, T)>: either None, or a value Some((v1, v2)), where (v1, v2) is a tuple of two values, both of type T. The parse_pair function doesn’t use an explicit return statement, so its return value is the value of the last (and the only) expression in its body:

match s.find(separator) {
    None => None,
    Some(index) => {
        ...
    }
}

The String type’s find method searches the string for a character that matches separator. If find returns None, meaning that the separator character doesn’t occur in the string, the entire match expression evaluates to None, indicating that the parse failed. Otherwise, we take index to be the separator’s position in the string.

match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
    (Ok(l), Ok(r)) => Some((l, r)),
    _ => None
}

This begins to show off the power of the match expression. The argument to the match is this tuple expression:

(T::from_str(&s[..index]), T::from_str(&s[index + 1..]))

The expressions &s[..index] and &s[index + 1..] are slices of the string, preceding and following the separator. The type parameter T’s associated from_str function takes each of these and tries to parse them as a value of type T, producing a tuple of results. This is what we match against:

(Ok(l), Ok(r)) => Some((l, r)),

This pattern matches only if both elements of the tuple are Ok variants of the Result type, indicating that both parses succeeded. If so, Some((l, r)) is the value of the match expression, and hence the return value of the function.

_ => None

The wildcard pattern _ matches anything, and ignores its value. If we reach this point, then parse_pair has failed, so we evaluate to None, again providing the return value of the function.

Now that we have parse_pair, it’s easy to write a function to parse a pair of floating-point coordinates and return them as a Complex<f64> value:

/// Parse a pair of floating-point numbers separated by a comma as a complex
/// number.
fn parse_complex(s: &str) -> Option<Complex<f64>> {
    match parse_pair(s, ',') {
        Some((re, im)) => Some(Complex { re, im }),
        None => None
    }
}

#[test]
fn test_parse_complex() {
    assert_eq!(parse_complex("1.25,-0.0625"),
               Some(Complex { re: 1.25, im: -0.0625 }));
    assert_eq!(parse_complex(",-0.0625"), None);
}

The parse_complex function calls parse_pair, builds a Complex value if the coordinates were parsed successfully, and passes failures along to its caller.

If you were reading closely, you may have noticed that we used a shorthand notation to build the Complex value. It’s common to initialize a struct’s fields with variables of the same name, so rather than forcing you to write Complex { re: re, im: im }, Rust lets you simply write Complex { re, im }. This is modeled on similar notations in JavaScript and Haskell.

Mapping from Pixels to Complex Numbers

The program needs to work in two related coordinate spaces: each pixel in the output image corresponds to a point on the complex plane. The relationship between these two spaces depends on which portion of the Mandelbrot set we’re going to plot, and the resolution of the image requested, as determined by command-line arguments. The following function converts from image space to complex number space:

/// Given the row and column of a pixel in the output image, return the
/// corresponding point on the complex plane.
///
/// `bounds` is a pair giving the width and height of the image in pixels.
/// `pixel` is a (column, row) pair indicating a particular pixel in that image.
/// The `upper_left` and `lower_right` parameters are points on the complex
/// plane designating the area our image covers.
fn pixel_to_point(bounds: (usize, usize),
                  pixel: (usize, usize),
                  upper_left: Complex<f64>,
                  lower_right: Complex<f64>)
    -> Complex<f64>
{
    let (width, height) = (lower_right.re - upper_left.re,
                           upper_left.im - lower_right.im);
    Complex {
        re: upper_left.re + pixel.0 as f64 * width  / bounds.0 as f64,
        im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64
        // Why subtraction here? pixel.1 increases as we go down,
        // but the imaginary component increases as we go up.
    }
}

#[test]
fn test_pixel_to_point() {
    assert_eq!(pixel_to_point((100, 100), (25, 75),
                              Complex { re: -1.0, im:  1.0 },
                              Complex { re:  1.0, im: -1.0 }),
               Complex { re: -0.5, im: -0.5 });
}

Figure 2-4 illustrates the calculation pixel_to_point performs.

(The complex points `upper_left` and `lower_right` mark            two opposite corners of a rectangle.            The rectangle is divided into `bounds.0` columns and `bounds.1` rows of pixels.            The upper left pixel is row 0, column 0,            and the lower right pixel is row `bounds.0 - 1`, column `bounds.1 - 1`.)
Figure 2-4. The relationship between the complex plane and the image’s pixels

The code of pixel_to_point is simply calculation, so we won’t explain it in detail. However, there are a few things to point out. Expressions with this form refer to tuple elements:

pixel.0

This refers to the first element of the tuple pixel.

pixel.0 as f64

This is Rust’s syntax for a type conversion: this converts pixel.0 to an f64 value. Unlike C and C++, Rust generally refuses to convert between numeric types implicitly; you must write out the conversions you need. This can be tedious, but being explicit about which conversions occur and when is surprisingly helpful. Implicit integer conversions seem innocent enough, but historically they have been a frequent source of bugs and security holes in real-world C and C++ code.

Plotting the Set

To plot the Mandelbrot set, for every pixel in the image, we simply apply escape_time to the corresponding point on the complex plane, and color the pixel depending on the result:

/// Render a rectangle of the Mandelbrot set into a buffer of pixels.
///
/// The `bounds` argument gives the width and height of the buffer `pixels`,
/// which holds one grayscale pixel per byte. The `upper_left` and `lower_right`
/// arguments specify points on the complex plane corresponding to the upper-
/// left and lower-right corners of the pixel buffer.
fn render(pixels: &mut [u8],
          bounds: (usize, usize),
          upper_left: Complex<f64>,
          lower_right: Complex<f64>)
{
    assert!(pixels.len() == bounds.0 * bounds.1);

    for row in 0 .. bounds.1 {
        for column in 0 .. bounds.0 {
            let point = pixel_to_point(bounds, (column, row),
                                       upper_left, lower_right);
            pixels[row * bounds.0 + column] =
                match escape_time(point, 255) {
                    None => 0,
                    Some(count) => 255 - count as u8
                };
        }
    }
}

This should all look pretty familiar at this point.

pixels[row * bounds.0 + column] =
    match escape_time(point, 255) {
        None => 0,
        Some(count) => 255 - count as u8
    };

If escape_time says that point belongs to the set, render colors the corresponding pixel black (0). Otherwise, render assigns darker colors to the numbers that took longer to escape the circle.

Writing Image Files

The image crate provides functions for reading and writing a wide variety of image formats, along with some basic image manipulation functions. In particular, it includes an encoder for the PNG image file format, which this program uses to save the final results of the calculation. In order to use image, add the following line to the [dependencies] section of Cargo.toml:

image = "0.13.0"

With that in place, we can write:

extern crate image;

use image::ColorType;
use image::png::PNGEncoder;
use std::fs::File;

/// Write the buffer `pixels`, whose dimensions are given by `bounds`, to the
/// file named `filename`.
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize))
    -> Result<(), std::io::Error>
{
    let output = File::create(filename)?;

    let encoder = PNGEncoder::new(output);
    encoder.encode(&pixels,
                   bounds.0 as u32, bounds.1 as u32,
                   ColorType::Gray(8))?;

    Ok(())
}

The operation of this function is pretty straightforward: it opens a file and tries to write the image to it. We pass the encoder the actual pixel data from pixels, and its width and height from bounds, and then a final argument that says how to interpret the bytes in pixels: the value ColorType::Gray(8) indicates that each byte is an eight-bit grayscale value.

That’s all straightforward. What’s interesting about this function is how it copes when something goes wrong. If we encounter an error, we need to report that back to our caller. As we’ve mentioned before, fallible functions in Rust should return a Result value, which is either Ok(s) on success, where s is the successful value, or Err(e) on failure, where e is an error code. So what are write_image’s success and error types?

When all goes well, our write_image function has no useful value to return; it wrote everything interesting to the file. So its success type is the unit type (), so called because it has only one value, also written (). The unit type is akin to void in C and C++.

When an error occurs, it’s because either File::create wasn’t able to create the file, or encoder.encode wasn’t able to write the image to it; the I/O operation returned an error code. The return type of File::create is Result<std::fs::File, std::io::Error>, while that of encoder.encode is Result<(), std::io::Error>, so both share the same error type, std::io::Error. It makes sense for our write_image function to do the same.

Consider the call to File::create. If that returns Ok(f) for a successfully opened File value f, then write_image can proceed to write the image data to f. But if File::create returns Err(e) for an error code e, write_image should immediately return Err(e) as its own return value. The call to encoder.encode must be handled similarly: failure should result in an immediate return, passing along the error code.

The ? operator exists to make these checks convenient. Instead of spelling everything out, and writing:

let output = match File::create(filename) {
    Ok(f) => { f }
    Err(e) => { return Err(e); }
};

you can use the equivalent and much more legible:

let output = File::create(filename)?;
Note

It’s a common beginner’s mistake to attempt to use ? in the main function. However, since main has no return value, this won’t work; you should use Result’s expect method instead. The ? operator is only useful within functions that themselves return Result.

There’s another shorthand we could use here. Because return types of the form Result<T, std::io::Error> for some type T are so common—this is often the right type for a function that does I/O—the Rust standard library defines a shorthand for it. In the std::io module, we have the definitions:

// The std::io::Error type.
struct Error { ... };

// The std::io::Result type, equivalent to the usual `Result`, but
// specialized to use std::io::Error as the error type.
type Result<T> = std::result::Result<T, Error>

If we bring this definition into scope with a use std::io::Result declaration, we can write write_image’s return type more tersely as Result<()>. This is the form you will often see when reading the documentation for functions in std::io, std::fs, and elsewhere.

A Concurrent Mandelbrot Program

Finally, all the pieces are in place, and we can show you the main function, where we can put concurrency to work for us. First, a nonconcurrent version for simplicity:

use std::io::Write;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() != 5 {
        writeln!(std::io::stderr(),
                 "Usage: mandelbrot FILE PIXELS UPPERLEFT LOWERRIGHT")
            .unwrap();
        writeln!(std::io::stderr(),
                 "Example: {} mandel.png 1000x750 -1.20,0.35 -1,0.20",
                 args[0])
            .unwrap();
        std::process::exit(1);
    }

    let bounds = parse_pair(&args[2], 'x')
        .expect("error parsing image dimensions");
    let upper_left = parse_complex(&args[3])
        .expect("error parsing upper left corner point");
    let lower_right = parse_complex(&args[4])
        .expect("error parsing lower right corner point");

    let mut pixels = vec![0; bounds.0 * bounds.1];

    render(&mut pixels, bounds, upper_left, lower_right);

    write_image(&args[1], &pixels, bounds)
        .expect("error writing PNG file");
}

After collecting the command-line arguments into a vector of Strings, we parse each one and then begin calculations.

let mut pixels = vec![0; bounds.0 * bounds.1];

A macro call vec![v; n] creates a vector n elements long whose elements are initialized to v, so the preceding code creates a vector of zeros whose length is bounds.0 * bounds.1, where bounds is the image resolution parsed from the command line. We’ll use this vector as a rectangular array of one-byte grayscale pixel values, as shown in Figure 2-5.

(Using a vector as a rectangular array of pixels.            The width is bounds.0, the height bounds.1.            Elements 0 through bounds.0-1 are the top row of pixels.            Element bounds.0*bounds.1-1 is the bottom right pixel.)
Figure 2-5. Using a vector as a rectangular array of pixels

The next line of interest is this:

render(&mut pixels, bounds, upper_left, lower_right);

This calls the render function to actually compute the image. The expression &mut pixels borrows a mutable reference to our pixel buffer, allowing render to fill it with computed grayscale values, even while pixels remains the vector’s owner. The remaining arguments pass the image’s dimensions, and the rectangle of the complex plane we’ve chosen to plot.

write_image(&args[1], &pixels, bounds)
    .expect("error writing PNG file");

Finally, we write the pixel buffer out to disk as a PNG file. In this case, we pass a shared (nonmutable) reference to the buffer, since write_image should have no need to modify the buffer’s contents.

The natural way to distribute this calculation across multiple processors is to divide the image into sections, one per processor, and let each processor color the pixels assigned to it. For simplicity, we’ll break it into horizontal bands, as shown in Figure 2-6. When all processors have finished, we can write out the pixels to disk.

(Dividing the pixel buffer into horizontal bands. The first thread renders the top band, the second thread the band below that, and so on.)
Figure 2-6. Dividing the pixel buffer into bands for parallel rendering

The crossbeam crate provides a number of valuable concurrency facilities, including a scoped thread facility that does exactly what we need here. To use it, we must add the following line to our Cargo.toml file:

crossbeam = "0.2.8"

Then, we must add the following line to the top of our main.rs file:

extern crate crossbeam;

Then we need to take out the single line calling render, and replace it with the following:

let threads = 8;
let rows_per_band = bounds.1 / threads + 1;

{
    let bands: Vec<&mut [u8]> =
        pixels.chunks_mut(rows_per_band * bounds.0).collect();
    crossbeam::scope(|spawner| {
        for (i, band) in bands.into_iter().enumerate() {
            let top = rows_per_band * i;
            let height = band.len() / bounds.0;
            let band_bounds = (bounds.0, height);
            let band_upper_left =
                pixel_to_point(bounds, (0, top), upper_left, lower_right);
            let band_lower_right =
                pixel_to_point(bounds, (bounds.0, top + height),
                               upper_left, lower_right);

            spawner.spawn(move || {
                render(band, band_bounds, band_upper_left, band_lower_right);
            });
        }
    });
}

Breaking this down in the usual way:

let threads = 8;
let rows_per_band = bounds.1 / threads + 1;

Here we decide to use eight threads.1 Then we compute how many rows of pixels each band should have. We round the row count upward, to make sure the bands cover the entire image even if the height isn’t a multiple of threads.

let bands: Vec<&mut [u8]> =
    pixels.chunks_mut(rows_per_band * bounds.0).collect();

Here we divide the pixel buffer into bands. The buffer’s chunks_mut method returns an iterator producing mutable, nonoverlapping slices of the buffer, each of which encloses rows_per_band * bounds.0 pixels—in other words, rows_per_band complete rows of pixels. The last slice that chunks_mut produces may contain fewer rows, but each row will contain the same number of pixels. Finally, the iterator’s collect method builds a vector holding these mutable, nonoverlapping slices.

Now we can put the crossbeam library to work:

crossbeam::scope(|spawner| { ... });

The argument |spawner| { ... } is a Rust closure expression. A closure is a value that can be called as if it were a function. Here, |spawner| is the argument list, and { ... } is the body of the function. Note that, unlike functions declared with fn, we don’t need to declare the types of a closure’s arguments; Rust will infer them, along with its return type.

In this case, crossbeam::scope calls the closure, passing as the spawner argument a value the closure can use to create new threads. The crossbeam::scope function waits for all such threads to finish execution before returning itself. This behavior allows Rust to be sure that such threads will not access their portions of pixels after it has gone out of scope, and allows us to be sure that when crossbeam::scope returns, the computation of the image is complete.

for (i, band) in bands.into_iter().enumerate() {

Here we iterate over the pixel buffer’s bands. The into_iter() iterator gives each iteration of the loop body exclusive ownership of one band, ensuring that only one thread can write to it at a time. We explain how this works in detail in Chapter 5. Then, the enumerate adapter produces tuples pairing each vector element with its index.

let top = rows_per_band * i;
let height = band.len() / bounds.0;
let band_bounds = (bounds.0, height);
let band_upper_left =
    pixel_to_point(bounds, (0, top), upper_left, lower_right);
let band_lower_right =
    pixel_to_point(bounds, (bounds.0, top + height),
                   upper_left, lower_right);

Given the index and the actual size of the band (recall that the last one might be shorter than the others), we can produce a bounding box of the sort render requires, but one that refers only to this band of the buffer, not the entire image. Similarly, we repurpose the renderer’s pixel_to_point function to find where the band’s upper-left and lower-right corners fall on the complex plane.

spawner.spawn(move || {
    render(band, band_bounds, band_upper_left, band_lower_right);
});

Finally, we create a thread, running the closure move || { ... }. This syntax is a bit strange to read: it denotes a closure of no arguments whose body is the { ... } form. The move keyword at the front indicates that this closure takes ownership of the variables it uses; in particular, only the closure may use the mutable slice band.

As we mentioned earlier, the crossbeam::scope call ensures that all threads have completed before it returns, meaning that it is safe to save the image to a file, which is our next action.

Running the Mandelbrot Plotter

We’ve used several external crates in this program: num for complex number arithmetic; image for writing PNG files; and crossbeam for the scoped thread creation primitives. Here’s the final Cargo.toml file including all those dependencies:

[package]
name = "mandelbrot"
version = "0.1.0"
authors = ["You <[email protected]>"]

[dependencies]
crossbeam = "0.2.8"
image = "0.13.0"
num = "0.1.27"

With that in place, we can build and run the program:

$ cargo build --release
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling bitflags v0.3.3
   ...
   Compiling png v0.4.3
   Compiling image v0.13.0
   Compiling mandelbrot v0.1.0 (file:///home/jimb/rust/mandelbrot)
    Finished release [optimized] target(s) in 42.64 secs
$ time target/release/mandelbrot mandel.png 4000x3000 -1.20,0.35 -1,0.20
real    0m1.750s
user    0m6.205s
sys     0m0.026s
$

Here, we’ve used the Unix time program to see how long the program took to run; note that even though we spent more than six seconds of processor time computing the image, the elapsed real time was less than two seconds. You can verify that a substantial portion of that real time is spent writing the image file by commenting out the code that does so; on the laptop where this code was tested, the concurrent version reduces the Mandelbrot calculation time proper by a factor of almost four. We’ll show how to substantially improve on this in Chapter 19.

This command should create a file called mandel.png, which you can view with your system’s image viewing program or in a web browser. If all has gone well, it should look like Figure 2-7.

Results from parallel Mandelbrot program
Figure 2-7. Results from parallel Mandelbrot program

Safety Is Invisible

In the end, the parallel program we ended up with is not substantially different from what we might write in any other language: we apportion pieces of the pixel buffer out among the processors; let each one work on its piece separately; and when they’ve all finished, present the result. So what is so special about Rust’s concurrency support?

What we haven’t shown here is all the Rust programs we cannot write. The code we looked at in this chapter partitions the buffer among the threads correctly, but there are many small variations on that code that do not (and thus introduce data races); not one of those variations will pass the Rust compiler’s static checks. A C or C++ compiler will cheerfully help you explore the vast space of programs with subtle data races; Rust tells you, up front, when something could go wrong.

In Chapters 4 and 5, we’ll describe Rust’s rules for memory safety. Chapter 19 explains how these rules also ensure proper concurrency hygiene. But for those to make sense, it’s essential to get a grounding in Rust’s fundamental types, which we’ll cover in the next chapter.

1 The num_cpus crate provides a function that returns the number of CPUs available on the current system.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.61.170