TL;DR

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.

Hello Rust

The best way to install Rust is to use rustup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# install rustup (got rustup, cargo, rustc, rustdoc) # https://rustup.rs/
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo --version
# cargo 1.65.0 (4bc8f24d3 2022-10-20)
rustc --version
# rustc 1.65.0 (897e37553 2022-11-02)
rustdoc --version
# rustdoc 1.65.0 (897e37553 2022-11-02)

# update Rust
rustup update

# install specific version
rustup install 1.49.0
# set default version
rustup default 1.49.0
cargo --version
# cargo 1.49.0 (d00d64df9 2020-12-05)
rustc --version
# rustc 1.49.0 (e1884a8e3 2020-12-29)
rustdoc --version
# rustdoc 1.49.0 (e1884a8e3 2020-12-29)
  • cargo is Rust’s compilation manager, package manager, and general-purpose tool. 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.
    • We usually let Cargo run rustdoc for us.
    • /// are documentation comments; the rustdoc utility knows how to parse them, together with the code they describe, and produce online documentation.

cargo commands:

  • cargo new <repo_name> creates a Rust package with some standard metadata.
    1. It creates a Cargo.toml file that holds metadata for the package. 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.
    2. It creates a .git metadata subdirectory and a .gitignore file.
      • Use --vsc none to skip the step of setting git metadata.
    3. The src subdirectory contains the actual Rust code.
    • Use --lib flag to create a library crate.
  • Invoke the cargo run command from any directory in the package to build and run our program.
    • Cargo places the executable in the target subdirectory at the top of the package.
  • cargo clean cleans up the generated files.
1
2
3
4
# vscode format on save: install rust-analyzer and add the following to settings
# "[rust]": {
#     "editor.defaultFormatter": "rust-lang.rust-analyzer"
# }

Functions

The fn keyword (pronounced “fun”) introduces a function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
}

By default, once a variable is initialized, its value can’t be changed; 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. 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.

A let statement declares a local variable. We don’t need to write out t’s type, as long as Rust can infer it from how the variable is used. Rust only infers types within function bodies: you must write out the types of function parameters and return values.

  • We can spell out t’s type like this: let t: u64 = m;

Rust has a return statement. If a function body ends with an expression that is not followed by a semicolon, that’s the function’s return value. Any block surrounded by curly braces can function as an expression.

1
2
3
4
5
// 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.

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.

Four-space indentation is standard Rust style.

Unit Tests

Rust has simple support for testing built into the language.

1
2
3
4
5
6
#[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);
}
// cargo test

We define a function named test_gcd. 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. 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.

Handling Command-line Arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// brings the standard library trait FromStr into scope
use std::str::FromStr;
// brings in the std::env module
use std::env;

fn main() {
    let mut numbers = Vec::new(); // Vec<u64>, inferred from push u64 and gcd(d, )
    for arg in env::args().skip(1) {
        numbers.push(u64::from_str(&arg).expect("error parsing argument"));
    }
    if numbers.len() == 0 {
        eprintln!("Usage: gcd NUMBER ...");
        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);
}

A trait is a collection of methods that types can implement. 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. u64::from_str parses the command-line arguments. Although we never use the name FromStr elsewhere in the program, a trait must be in scope in order to use its methods.

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 std::env module provides several useful functions and types for interacting with the execution environment, including the args function. The 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.

Iterators also include a broad selection of methods you can use directly. For example, the first value produced by the iterator returned by args is always the name of the program being run. The iterator’s skip method produces a new iterator that omits that first value.

u64::from_str attempts to parse arg as an unsigned 64-bit integer. It 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
1
2
3
4
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Functions that do anything that might fail can return Result types whose Ok variants carry successful results and whose Err variants carry an error code indicating what went wrong. Unlike most modern languages, Rust does not have exceptions: all errors are handled using either Result or panic.

We use Result’s expect method to check the success of our parse. If the result is Ok(v), expect simply returns v itself. If the result is an Err(e), expect prints a message that includes a description of e and exits the program immediately.

A vector could be of any size—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. 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. Since numbers owns the vector, Rust automatically frees it when numbers goes out of scope at the end of main.

The eprintln! macro writes our error message to the standard error 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.

C and C++ 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.

View the standard library documentation in your browser with rustup doc --std.

Serving Pages to the Web

A Rust package, whether a library or an executable, is called a crate. We need only name those crates directly in our Cargo.toml; cargo takes care of bringing in whatever other crates those need in turn.

1
2
3
4
5
6
7
8
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "1.0.8"
serde = { version = "1.0", features = ["derive"] }

Rust crates that have reached version 1.0, as these have, follow the “semantic versioning” rules: until the major version number 1 changes, newer versions should always be compatible extensions of their predecessors. So if we test our program against version 1.2 of some crate, it should still work with versions 1.3, 1.4, and so on; but version 2.0 could introduce incompatible changes. When we simply request version "1" of a crate in a Cargo.toml file, Cargo will use the newest available version of the crate before 2.0.

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

Crates can have optional features: parts of the interface or implementation that not all users need, but that nonetheless make sense to include in that crate. The serde crate offers a wonderfully terse way to handle data from web forms, but according to serde’s documentation, it is only available if we select the crate’s derive feature, so we’ve requested it in our Cargo.toml file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use actix_web::{web, App, HttpResponse, HttpServer};
fn main() {
    // the whole part inside parentheses is a closure
    let server = HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(get_index))
            .route("gcd", web::post().to(post_gcd))
    });
    println!("Serving on http://localhost:3000...");
    server
        .bind("127.0.0.1:3000")
        .expect("error binding server to address")
        .run()
        .expect("error running server");
}
fn get_index() -> HttpResponse {
    HttpResponse::Ok().content_type("text/html").body(
        r#"
            <title>GCD Calculator</title>
            <form action="/gcd" method="post">
            <input type="text" name="n"/>
            <input type="text" name="m"/>
            <button type="submit">Compute GCD</button>
            </form>
        "#,
    )
}
#[derive(Deserialize)]
struct GcdParameters {
    n: u64,
    m: u64,
}
fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse {
    if form.n == 0 || form.m == 0 {
        return HttpResponse::BadRequest()
            .content_type("text/html")
            .body("Computing the GCD with zero is boring.");
    }
    let response = format!(
        "The greatest common divisor of the numbers {} and {} \
        is <b>{}</b>\n",
        form.n,
        form.m,
        gcd(form.n, form.m)
    );
    HttpResponse::Ok().content_type("text/html").body(response)
}
  • When we write use actix_web::{...}, each of the names listed inside the curly brackets becomes directly usable in our code; instead of having to spell out the full name actix_web::HttpResponse each time we use it, we can simply refer to it as HttpResponse.
  • || { App::new() ... } is a Rust closure expression. A closure is a value that can be called as if it were a function.
    • This closure takes no arguments, but if it did, their names would appear between the || vertical bars.
    • The { ... } is the body of the closure.
  • When we start our server, Actix starts a pool of threads to handle incoming requests. Each thread calls our closure to get a fresh copy of the App value that tells it how to route and handle requests.
    • The closure calls App::new to create a new, empty App and then calls its route method to add routes for path "/". The route method returns the same App it was invoked on, now enhanced with the new route.
    • Since there’s no semicolon at the end of the closure’s body, the App is the closure’s return value, ready for the HttpServer thread to use.
  • HttpResponse::Ok() represents an HTTP 200 OK status, indicating that the request succeeded. We call its content_type and body methods to fill in the details of the response; each call returns the HttpResponse it was applied to, with the modifications made.
  • 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.
    • We can always ensure the string ends where we intend by using more hash marks around the quotes than ever appear in the text.
  • Placing a #[derive(Deserialize)] attribute above a type definition tells the serde crate to examine the type when the program is compiled and automatically generate code to parse a value of this type from data in the format that HTML forms use for POST requests.
    • This attribute is sufficient to let you parse a GcdParameters value from almost any sort of structured data: JSON, YAML, TOML, or any one of a number of other textual and binary formats.
  • For a function to serve as an Actix request handler, its arguments must all have types Actix knows how to extract from an HTTP request. Actix knows how to extract a value of any type web::Form<T> from an HTTP request if, and only if, T can be deserialized from HTML form POST data. Since we’ve placed the #[derive(Deserialize)] attribute on GcdParameters type definition, Actix can deserialize it from form data, so request handlers can expect a web::Form<GcdParameters> value as a parameter.
    • These relationships between types and functions are all worked out at compile time; if you write a handler function with an argument type that Actix doesn’t know how to handle, the Rust compiler lets you know of your mistake immediately.
  • The format! macro is just like the println! macro, except that instead of writing the text to the standard output, it returns it as a string.

Rust programmers typically gather all their 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.

Concurrency

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 your program compiles, it is free of data races. All Rust functions are thread-safe.

Consider the following loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use num::Complex;

fn square_add_loop(c: f64) {
    let mut x = 0.;
    // In real life, Rust can see that z is never used for anything and so might not bother computing its value.
    loop {
        x = x * x + c;
    }
}

fn complex_square_add_loop(c: Complex<f64>) {
    let mut z = Complex { re: 0.0, im: 0.0 };
    loop {
        z = z * z + c;
    }
}
struct Complex<T> {
    /// Real portion of the complex number
    re: T,
    /// Imaginary portion of the complex number
    im: T,
}
  • Some experimentation shows that if c in square_add_loop is greater than 0.25 or less than –2.0, then z eventually becomes infinitely large; otherwise, it stays somewhere in the neighborhood of zero.
  • Complex is a generic structure. Read the <T> after the type name as “for any type T.”

The Mandelbrot set is defined as the set of complex numbers c for which z does not fly out to infinity. 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.

  • It’s been shown that, if z ever once leaves the circle of radius 2 centered at the origin, it will definitely fly infinitely far away from the origin eventually.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 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 2 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: usize) -> Option<usize> {
    let mut z = Complex { re: 0.0, im: 0.0 };
    for i in 0..limit {
        if z.norm_sqr() > 4.0 {
            return Some(i);
        }
        z = z * z + c;
    }
    None
}

enum Option<T> {
    None,
    Some(T),
}
  • 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.
  • 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. Option is a generic type.

The program takes several command-line arguments controlling the resolution of the image we’ll write and the portion of the Mandelbrot set the image shows.

The definition of parse_pair is a generic function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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`. `separator` must be an
/// ASCII character.
///
/// 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,
        // index is the separator’s position in the string
        Some(index) => {
            match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
                // this pattern matches only if both elements of the tuple are Ok variants of the Result type
                (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)));
}

/// 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);
}
  • Read the <T: FromStr> 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++.
  • T a type parameter of parse_pair. When you use a generic function, Rust will often infer type parameters of a generic function, and you won’t need to write them out as we did in the test code.
  • 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.
  • 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.
  • The wildcard pattern _ matches anything and ignores its 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 } as a shorthand notation.

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:

1
2
3
4
5
fn pixel_to_point(bounds: (usize, usize),
                  pixel: (usize, usize),
                  upper_left: Complex<f64>,
                  lower_right: Complex<f64>)
    -> Complex<f64>
  • pixel.0 refers to the first element of the tuple pixel.
  • pixel.0 as f64 converts pixel.0 to an f64 value. Rust generally refuses to convert between numeric types implicitly.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// 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
                };
        }
    }
}
  • 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.

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.

The unit type (zero-tuple) () has only one value, also written (). The unit type is akin to void in C and C++.

Handle File::create’s result:

1
2
3
4
5
6
7
8
let output = match File::create(filename) {
    Ok(f) => f,
    Err(e) => {
        return Err(e);
    }
};
// shorthand
let output = File::create(filename)?;
  • On success, let output be the File carried in the Ok value. On failure, pass along the error to the caller.
  • This kind of match statement is such a common pattern in Rust that the language provides the ? operator as shorthand for the whole thing.
    • Attempting to use ? in the main function won’t work because it doesn’t return a value. Use a match statement, or one of the shorthand methods like unwrap and expect. There’s also the option of simply changing main to return a Result.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      fn main() {
          // ...
          match fs::write(&args.output, &data) {
              Ok(_) => {},
              Err(e) => {
                  eprintln!("{} failed to write to file '{}': {:?}",
                      "Error:".red().bold(), args.filename, e);
                  std::process::exit(1);
              }
          };
      }
      

The macro call vec![v; n] creates a vector n elements long whose elements are initialized to v.

The crossbeam crate provides a number of valuable concurrency facilities.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);

            // thread created here
            spawner.spawn(move |_| {
                render(band, band_bounds, band_upper_left, band_lower_right);
            });
        }
    }).unwrap();
}
  • The argument |spawner| { ... } is a Rust closure that expects a single argument, spawner.
    • 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.
  • 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.
    • 调用方传 spawner 参数给闭包,这一参数不用手动创建。
  • The move keyword at the front indicates that this closure takes ownership of the variables it uses.
  • The argument list |_| means that the closure takes one argument, which it doesn’t use (another spawner for making nested threads).
    • 需要嵌套创建线程时可以写成 |spawner_nested|

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

Filesystems and Command-Line Tools

Rust gives programmers a toolbox they can use to assemble slick command-line interfaces that replicate or extend the functionality of existing tools.

The #[derive(Debug)] attribute tells the compiler to generate some extra code that allows us to format the Arguments struct with {:?} in println!.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#[derive(Debug)]
struct Arguments {
    target: String,
    replacement: String,
    filename: String,
    output: String,
}

fn main() {
    let args = parse_args();
    println!("{:?}", args);

    println!("{}", args);
    // = help: the trait `std::fmt::Display` is not implemented for `Arguments`
    // = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
}

References