Rust Programming Cookbook

4.5 (2 reviews total)
By Claus Matzinger
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Starting Off with Rust

About this book

Rust 2018, Rust's first major milestone since version 1.0, brings more advancement in the Rust language. The Rust Programming Cookbook is a practical guide to help you overcome challenges when writing Rust code.

This Rust book covers recipes for configuring Rust for different environments and architectural designs, and provides solutions to practical problems. It will also take you through Rust's core concepts, enabling you to create efficient, high-performance applications that use features such as zero-cost abstractions and improved memory management. As you progress, you'll delve into more advanced topics, including channels and actors, for building scalable, production-grade applications, and even get to grips with error handling, macros, and modularization to write maintainable code. You will then learn how to overcome common roadblocks when using Rust for systems programming, IoT, web development, and network programming. Finally, you'll discover what Rust 2018 has to offer for embedded programmers.

By the end of the book, you'll have learned how to build fast and safe applications and services using Rust.

Publication date:
October 2019
Publisher
Packt
Pages
444
ISBN
9781789530667

 

Starting Off with Rust

The Rust ecosystem has grown considerably over the last year, and the 2018 edition, in particular, brought a significant push toward stabilization. The tooling is developing and important libraries are maturing to a point where many bigger companies use Rust in production.

One of the features of Rust is a steep learning curve—which is mostly due to a fundamental change in how to think about memory allocation. It is not uncommon for experienced programmers in other languages (such as C#) to feel overwhelmed with the way things are done in Rust. In this chapter, we will try to overcome this and lower the bar to get started!

In this chapter, we will cover the following recipes:

  • Getting everything ready
  • Working with the command line I/O
  • Creating and using data types
  • Controlling execution flow
  • Splitting your code with crates and modules
  • Writing tests and benchmarks
  • Documenting your code
  • Testing your documentation
  • Sharing code among types
  • Sequence types in Rust
  • Debugging Rust

 

Setting up your environment

Since the programming language comes with a variety of toolchains, tools, linkers, and compiler versions, choosing the best-fitting variation is not easy. Additionally, Rust works on all major operating systems—which adds another variable. 

However, installing Rust has become a trivial task when using rustup (https://rustup.rs/). On the website, a helpful script (or installer on Windows) that takes care of retrieving and installing the required components can be downloaded. The same tool lets you switch between and update (and uninstall) these components as well. This is the recommended way.

Choosing to use the Microsoft Visual Studio Compiler (MSVC) together with Rust requires that you install additional software such as the Visual C++ runtime and compiler tools. 

To write code, an editor is also required. Since Visual Studio Code sports some Rust parts, it is a great choice together with the Rust extension. It's an open source editor developed by Microsoft and is well received across the world and the Rust community. In this recipe, we will install the following components:

  • Visual Studio Code (https://code.visualstudio.com/)
  • rustup (https://rustup.rs)
  • rustc (and the rest of the compiler toolchains)
  • cargo
  • RLS (short for Rust Language Server—this is for autocompletion)
  • Rust language support for Visual Studio Code

Getting ready

On a computer running either macOS, Linux, or Windows, only a web browser and internet connection are required. Bear in mind that the Windows installation works a little bit different from the *nix systems (Linux and macOS), which use scripts. 

How to do it...

Each of the parts requires us to navigate to their respective websites, download the installer, and follow their instructions:

  1. Open the browser and navigate to https://rustup.rs and https://code.visualstudio.com/.
  2. Choose the installers fit for your operating system.
  3. After downloading, run the installers and follow their instructions, choosing the stable branches.
  4. Once successfully installed, we'll go deeper into each installation.

Now, let's go behind the scenes to understand the installation better

Managing the Rust installation with rustup.rs

To test whether the installation of the Rust toolchain with rustup was successful, the rustc command is available to run in Terminal (or PowerShell on Windows):

$ rustc --version
rustc 1.33.0 (2aa4c46cf 2019-02-28)

Note that you will have a later version when you are running this. It doesn't matter if you stick to the 2018 edition for your code.

Rust requires a native linker to be available on your system. On Linux or Unix systems (such as macOS), Rust calls cc for linking, whereas on Windows, the linker of choice is Microsoft Visual Studio's linker, which depends on having Microsoft Visual C++ Build Tools installed. While it's possible to use an open source toolchain on Windows as well, this exercise is left for more advanced users.

Even with the 2018 edition, some useful features are still only available on nightly. To install the nightly edition of rustc, perform these steps:

  1. Run rustup install nightly (use nightly-msvc on Windows if you are not using the GNU toolchain) in a Terminal or PowerShell window.
  2. After the command finishes, the default toolchain (used in cargo) can be switched using rustup default nightly.

Installing Visual Studio Code and extensions

In its vanilla version, Visual Studio Code comes with syntax highlighting for many languages. However, for autocompletion or/and checking syntax, an extension is required. The Rust project supplies this extension:

  1. Open Visual Studio Code.
  2. Use Ctrl + P (cmd + P on macOS) to open the command-line interface, then type ext install rust-lang.rust to install the extension. The process should look like this:

The extension uses RLS to do static code analysis and provide completion and syntax checking. The extension should install the RLS component automatically, but sometimes it will fail to do this. One solution is to add the following configuration to Visual Studio Code's settings.json file (use Ctrl + P/cmd + P to find it):

{
"rust-client.channel":"stable"
}

Alternatively, rustup will also install RLS with the rustup component add rls command.

Troubleshooting

Occasionally, updating the tools will lead to errors that files are missing or cannot be overwritten. This can be for a wide range of reasons, but a full reset of the installations can help. On Linux or macOS systems, the following command takes care of deleting anything rustup installed:

$ rm -Rf ~/.rustup

Windows's PowerShell now supports many Linux-like commands:

PS> rm ~/.rustup

This leads to the same result. After deleting the current installation, install rustup from scratch—this should install the latest version.

Now, let's go behind the scenes to understand the code better.

How it works...

The shell script, rustup.sh, is a great way to install Rust and it is the primary way to install Rust and other components today. In fact, it is common to use the script also in CI systems to install the compiler and other tools. 

rustup is an open source project maintained by the Rust project and can be found on GitHub: https://github.com/rust-lang/rustup.rs

We've successfully learned how to set up our environment. Now let's move on to the next recipe.

 

Working with the command line I/O

The traditional way of communicating with the user on the command line is using standard streams. Rust includes helpful macros to deal with these simple cases. In this recipe, we will explore the basic workings of the classic Hello World program.

How to do it...

In just five steps, we will explore command line I/O and formatting:

  1. Open a Terminal window (PowerShell on Windows) and run the cargo new hello-world command, which creates a new Rust project in a hello-world folder.
  2. Once created, change into the directory with cd hello-world and open src/main.rs with a Visual Studio Code. The default code generated by cargo looks like this:
fn main() {
println!("Hello, world!");
}
  1. Let's expand it! These are variations on the preceding traditional print statement, showing some formatting options, parameters, and writing on streams, among other things. Let's start with some common prints (and imports):
use std::io::{self, Write};
use std::f64;

fn main() {
println!("Let's print some lines:");
println!();
println!("Hello, world!");
println!("{}, {}!", "Hello", "world");
print!("Hello, ");
println!("world!");

However, we can do much more complex argument combinations:

    println!("Arguments can be referred to by their position: {0}, 
{1}! and {1}, {0}! are built from the same arguments", "Hello",
"world");

println!("Furthermore the arguments can be named: \"{greeting},
{object}!\"", greeting = "Hello", object = "World");

println!("Number formatting: Pi is {0:.3} or {0:.0} for short",
f64::consts::PI);

println!("... and there is more: {0:>0width$}={0:>width$}=
{0:#x}", 1535, width = 5);

let _ = write!(&mut io::stdout(), "Underneath, it's all writing
to a stream...");
println!();

println!("Write something!");
let mut input = String::new();
if let Ok(n) = io::stdin().read_line(&mut input) {
println!("You wrote: {} ({} bytes) ", input, n);
}
else {
eprintln!("There was an error :(");
}
}

This should provide several variations of reading and writing to the console.

  1. Go back to Terminal and navigate to the directory where Cargo.toml is located.
  2. Use cargo run to see the snippet's output:
$ cargo run
Compiling hello-world v0.1.0 (/tmp/hello-world)
Finished dev [unoptimized + debuginfo] target(s) in 0.37s
Running 'target/debug/hello-world'
Let's print some lines:

Hello, world!
Hello, world!
Hello, world!
Arguments can be referred to by their position: Hello, world! and world, Hello! are built from the same arguments
Furthermore the arguments can be named: "Hello, World!"
Number formatting: Pi is 3.142 or 3 for short
... and there is more: 01535= 1535=0x5ff
Underneath, it's all writing to a stream...
Write something!
Hello, world!
You wrote: Hello, world!
(14 bytes)

Each line in the output represents a way to print text to the console! We recommend playing with the variations and seeing how it changes the result. On a side note, rustc will check for the correct number of arguments in any println!() or format!() call.

Now, let's go behind the scenes to understand the code better.

How it works...

Let's go through the code to understand the execution flow.

cargo is described in depth in Chapter 2Managing Projects with Cargo, in this book.

The initial snippet is generated when cargo new hello-world is executed in step 1. As a project of type binary, a main function is required and rustc will be looking for it. Upon calling cargo run, cargo orchestrates compilation (with rustc) and linking (msvc on Windows, cc on *nix) and runs the resultant binary via its entry point: the main function (step 5). 

In the function we create in step 3, we write a series of print!/println!/eprintln! statements, which are Rust macros. These macros facilitate the writing to the standard output or standard error channels of a command-line application and include additional arguments. In fact, if arguments are missing, the compiler won't compile the program.

Rust's macros work directly on the syntax tree of the language, providing type safety and the ability to check the parameters and arguments. Therefore, they can be seen as a function call with a few special abilities—but more on that in Chapter 6, Expressing Yourself with Macros.   

The various arguments and the template string are combined using formatters, a powerful way to add real variables to the output without the need of concatenations or similar workarounds. This will reduce the number of allocations, considerably improving performance and memory efficiency. There is a wide range of how to format data types; to understand it more deeply, check out Rust's excellent documentation (https://doc.rust-lang.org/std/fmt/). 

The last step then shows the output that the various combinations produced.

We've successfully learned to work with the command line I/O. Now, let's move on to the next recipe.

 

Creating and using data types

Rust features all of the basic types: signed and unsigned integers up to 64 bits in width; floating-point types up to 64 bits; character types; and Booleans. Of course, any program will need more complex data structures to remain readable.

If you are unfamiliar with unit tests in Rust (or in general), we suggest going through the Writing tests and benchmarks recipe here in this chapter first.

In this recipe, we'll look at good basic practices to create and use data types. 

How to do it...

Let's use Rust's unit tests as a playground for some data type experiments:

  1. Create a new project using cargo new data-types -- lib and use an editor to open the projects directory.
  2. Open src/lib.rs in your favorite text editor (Visual Studio Code).
  3. In there, you will find a small snippet to run a test:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
  1. Let's replace the default test to play with various standard data types. This test uses a few ways to work with data types and their math functions, as well as mutability and overflows:
    #[test]
fn basic_math_stuff() {
assert_eq!(2 + 2, 4);

assert_eq!(3.14 + 22.86, 26_f32);

assert_eq!(2_i32.pow(2), 4);
assert_eq!(4_f32.sqrt(), 2_f32);

let a: u64 = 32;
let b: u64 = 64;

// Risky, this could overflow
assert_eq!(b - a, 32);
assert_eq!(a.overflowing_sub(b), (18446744073709551584,
true));
let mut c = 100;
c += 1;
assert_eq!(c, 101);
}
  1. Having the basic numeric types covered, let's check a major limitation: overflows! Rust panics when an overflow occurs, so we are going to expect that with the #[should_panic] attribute (the test will actually fail if it doesn't panic):
    #[test]
#[should_panic]
fn attempt_overflows() {
let a = 10_u32;
let b = 11_u32;

// This will panic since the result is going to be an
// unsigned type which cannot handle negative numbers
// Note: _ means ignore the result
let _ = a - b;
}
  1. Next, let's create a custom type as well. Rust's types are structs and they add no overhead in memory. The type features a new() (constructor by convention) and a sum() function, both of which we'll call in a test function:

// Rust allows another macro type: derive. It allows to "auto-implement"
// supported traits. Clone, Debug, Copy are typically handy to derive.
#[derive(Clone, Debug, Copy)]
struct MyCustomStruct {
a: i32,
b: u32,
pub c: f32
}

// A typical Rust struct has an impl block for behavior
impl MyCustomStruct {

// The new function is static function, and by convention a
// constructor
pub fn new(a: i32, b: u32, c: f32) -> MyCustomStruct {
MyCustomStruct {
a: a, b: b, c: c
}
}

// Instance functions feature a "self" reference as the first
// parameter
// This self reference can be mutable or owned, just like other
// variables
pub fn sum(&self) -> f32 {
self.a as f32 + self.b as f32 + self.c
}
}
  1. To see the new struct function in action, let's add a test to do some and clone memory tricks with types (note: pay attention to the asserts):
    use super::MyCustomStruct;

#[test]
fn test_custom_struct() {
assert_eq!(mem::size_of::<MyCustomStruct>(),
mem::size_of::<i32>() + mem::size_of::<u32>() +
mem::size_of::<f32>());

let m = MyCustomStruct::new(1, 2, 3_f32);
assert_eq!(m.a, 1);
assert_eq!(m.b, 2);
assert_eq!(m.c, 3_f32);

assert_eq!(m.sum(), 6_f32);
let m2 = m.clone();
assert_eq!(format!("{:?}", m2), "MyCustomStruct { a: 1, b:
2,
c: 3.0 }");

let mut m3 = m;
m3.a = 100;

assert_eq!(m2.a, 1);
assert_eq!(m.a, 1);
assert_eq!(m3.a, 100);
}
  1. Lastly, let's see whether all of that works. Run cargo test in the data-types directory and you should see the following output:
$ cargo test
Compiling data-types v0.1.0 (Rust-Cookbook/Chapter01/data-types)
warning: method is never used: `new`
--> src/lib.rs:13:5
|
13 | pub fn new(a: i32, b: u32, c: f32) -> MyCustomStruct {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default

warning: method is never used: `sum`
--> src/lib.rs:19:5
|
19 | pub fn sum(&self) -> f32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^

Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running target/debug/deps/data_types-33e3290928407ff5

running 3 tests
test tests::basic_math_stuff ... ok
test tests::attempt_overflows ... ok
test tests::test_custom_struct ... ok

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

Doc-tests data-types

running 0 tests

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

Now, let's go behind the scenes to understand the code better.

How it works...

This recipe played with several concepts, so let's unpack them here. After setting up a library to work with unit tests as our playground in step 1 to step 3, we create a first test to work on some built-in data types to go through the basics in step 4 and step 5. Since Rust is particularly picky about type conversions, the test applies some math functions on the outcomes and inputs of different types.

For experienced programmers, there is nothing new here, except for the fact that there is an overflow_sub() type operation that allows for overflowing operations. Other than that, Rust might be a bit more verbose thanks to the (intentional) lack of implicit casting. In step 5, we intentionally provoke an overflow, which leads to a runtime panic (and is the test result we are looking for). 

As shown in step 5, Rust offers struct as the foundation for complex types, which can have attached implementation blocks as well as derived (#[derive(Clone, Copy, Debug)]) implementations (such as the Debug and Copy traits). In step 6, we go through using the type and its implications:

  • No overhead on custom types: struct has exactly the size that the sum of its properties has
  • Some operations implicitly invoke a trait implementationsuch as the assignment operator or the Copy trait (which is essentially a shallow copy)
  • Changing property values requires the mutability of the entire struct function

There are a few aspects that work like that because the default allocation strategy is to prefer the stack whenever possible (or if nothing else is mentioned). Therefore, a shallow copy of the data performs a copy of the actual data as opposed to a reference to it, which is what happens with heap allocations. In this case, Rust forces an explicit call to clone() so the data behind the reference is copied as well. 

We've successfully learned how to create and use data types. Now, let's move on to the next recipe.

 

Controlling execution flow

In Rust, controlling the execution flow of a program goes beyond simple if and while statements. We will see how to do that in this recipe.

How to do it...

For this recipe, the steps are as follows:

  1. Create a new project using cargo new execution-flow -- lib and open the project in an editor.
  2. Basic conditionals such as if statements work just like in any other language, so let's start with those and replace the default mod tests { ... } statement in the file:
#[cfg(test)]
mod tests {
#[test]
fn conditionals() {
let i = 20;
// Rust's if statement does not require parenthesis
if i < 2 {
assert!(i < 2);
} else if i > 2 {
assert!(i > 2);
} else {
assert_eq!(i, 2);
}
}
}
  1. Conditionals in Rust can do much more! Here is an additional test to show what they can do—add it before the last closing parenthesis:
    #[test]
fn more_conditionals() {
let my_option = Some(10);

// If let statements can do simple pattern matching
if let Some(unpacked) = my_option {
assert_eq!(unpacked, 10);
}

let mut other_option = Some(2);
// there is also while let, which does the same thing
while let Some(unpacked) = other_option {

// if can also return values in assignments
other_option = if unpacked > 0 {
Some(unpacked - 1)
} else {
None
}
}
assert_eq!(other_option, None)
}
  1. A conditional isn't the only statement that can be used to change the flow of execution. There is, of course, also the loop and its variations. Let's add another test for those as well, starting with a few basics:
    #[test]
fn loops() {

let mut i = 42;
let mut broke = false;

// a basic loop with control statements
loop {
i -= 1;
if i < 2 {
broke = true;
break;
} else if i > 2 {
continue;
}
}
assert!(broke);

// loops and other constructs can be named for better
readability ...
'outer: loop {
'inner: loop {
break 'inner; // ... and specifically jumped out of
}
break 'outer;
}
  1. Next, we will add more code to the test to see that loops are regular statements that can return values and that ranges can be used in for loops as well:
        let mut iterations: u32 = 0;

let total_squared = loop {
iterations += 1;

if iterations >= 10 {
break iterations.pow(2);
}
};
assert_eq!(total_squared, 100);

for i in 0..10 {
assert!(i >= 0 && i < 10)
}

for v in vec![1, 1, 1, 1].iter() {
assert_eq!(v, &1);
}
}

  1. With these three tests prepared, let's run cargo test to see them working:
$ cargo test
Compiling execution-flow v0.1.0 (Rust-Cookbook/Chapter01/execution-flow)
warning: value assigned to `broke` is never read
--> src/lib.rs:20:17
|
20 | let mut broke = false;
| ^^^^^
|
= note: #[warn(unused_assignments)] on by default
= help: maybe it is overwritten before being read?

Finished dev [unoptimized + debuginfo] target(s) in 0.89s
Running target/debug/deps/execution_flow-5a5ee2c7dd27585c

running 3 tests
test tests::conditionals ... ok
test tests::loops ... ok
test tests::more_conditionals ... ok

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

Now, let's go behind the scenes to understand the code better.

How it works...

Although not vastly different from many languages' control statements, the basic constructs in Rust can change the way you think about variable assignments. It certainly transformed our mental models to be more data-focused. This means that instead of thinking if this condition is reached, assign this other value to a variable, a reversed assign this other value to a variable if this condition is reached—or shorter transform this variable if this condition applies—may take over. 

This is the functional stream in the Rust programming language and it lends itself well to shortening and focusing the important parts of a piece of code. Similar implications can be made from the loop constructs since everything is a scope and has a return value. Using these capabilities will make every program a lot more readable and shorter, especially if it's just simple operations. 

We've successfully learned how to control execution flow. Now, let's move on to the next recipe.

 

Splitting your code with crates and modules

Rust knows two types of code units: crates and modules. A crate is an external library, complete with its own Cargo.toml configuration file, dependencies, tests, and code. Modules, on the other hand, split the crate into logical parts that are only visible to the user if they import specific functions. Since the 2018 edition of Rust, the difference in using these structural encapsulations has been minimized. 

Getting ready

This time, we are going to create two projects: one that offers some type of function and another one to use it. Therefore, use cargo to create both projects: cargo new rust-pilib --lib and cargo new pi-estimator. The second command creates a binary executable so we can run the compilation result, while the former is a library (crate). 

This recipe is going to create a small program that prints out estimations of pi () and rounds them to two decimal places. It's nothing fancy and easy for anyone to understand.

Naming crates is hard. The main repository (https://crates.io/) is very permissive and has already seen name squatting (where people reserve names with the intent to sell them—think of names such as YouTube or Facebook, which would make nice API client names for these companies), and many crates are re-implementations of C libraries or wrap them. A good practice is to call the repository or directory rust-mycoolCwrapper and use mycoolCwrapper to name the crate itself. This way, only issues specific to your crate come in while the name is easy to guess in people's dependencies!

How to do it...

In just a few steps, we will be working with different modules:

  1. First, we are going to implement the rust-pilib crate. As a simple example, it estimates the constant pi using the Monte Carlo method. This method is somewhat similar to throwing darts at a dartboard and counting the hits. Read more on Wikipedia (https://en.wikipedia.org/wiki/Monte_Carlo_method). Add to the tests submodule this snippet:
use rand::prelude::*;

pub fn monte_carlo_pi(iterations: usize) -> f32 {
let mut inside_circle = 0;
for _ in 0..iterations {

// generate two random coordinates between 0 and 1
let x: f32 = random::<f32>();
let y: f32 = random::<f32>();

// calculate the circular distance from 0, 0
if x.powi(2) + y.powi(2) <= 1_f32 {
// if it's within the circle, increase the count
inside_circle += 1;
}
}
// return the ratio of 4 times the hits to the total
iterations
(4_f32 * inside_circle as f32) / iterations as f32
}
  1. Additionally, the Monte Carlo method uses a random number generator. Since Rust doesn't come with one in its standard library, an external crate is required! Modify Cargo.toml of the rust-pilib project to add the dependency:
[dependencies]
rand = "^0.5"
  1. As good engineers, we are also going to add tests to our new library. Replace the original test module with the following tests to approximate pi using the Monte Carlo method: 
#[cfg(test)]
mod tests {
// import the parent crate's functions
use super::*;

fn is_reasonably_pi(pi: f32) -> bool {
pi >= 3_f32 && pi <= 4.5_f32
}

#[test]
fn test_monte_carlo_pi_1() {
let pi = monte_carlo_pi(1);
assert!(pi == 0_f32 || pi == 4_f32);
}

#[test]
fn test_monte_carlo_pi_500() {
let pi = monte_carlo_pi(500);
assert!(is_reasonably_pi(pi));
}

We can even go beyond 500 iterations:

    #[test]
fn test_monte_carlo_pi_1000() {
let pi = monte_carlo_pi(1000);
assert!(is_reasonably_pi(pi));
}

#[test]
fn test_monte_carlo_pi_5000() {
let pi = monte_carlo_pi(5000);
assert!(is_reasonably_pi(pi));
}
}

  1. Next, let's run the tests so we are certain of the quality of our product. Run cargo test in the root of the rust-pilib project. The output should be somewhat like this:
$ cargo test
Compiling libc v0.2.50
Compiling rand_core v0.4.0
Compiling rand_core v0.3.1
Compiling rand v0.5.6
Compiling rust-pilib v0.1.0 (Rust-Cookbook/Chapter01/rust-pilib)
Finished dev [unoptimized + debuginfo] target(s) in 3.78s
Running target/debug/deps/rust_pilib-d47d917c08b39638

running 4 tests
test tests::test_monte_carlo_pi_1 ... ok
test tests::test_monte_carlo_pi_500 ... ok
test tests::test_monte_carlo_pi_1000 ... ok
test tests::test_monte_carlo_pi_5000 ... ok

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

Doc-tests rust-pilib

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  1. Now we want to offer the crate's feature(s) to the user, which is why we created a second project for the user to execute. Here, we declare to use the other library as an external crate first. Add the following to Cargo.toml in the pi-estimator project:
[dependencies]
rust-pilib = { path = '../rust-pilib', version = '*'}
  1. Then, let's take a look at the src/main.rs file. Rust looks there to find a main function to run and, by default, it simply prints Hello, World! to standard output. Let's replace that with a function call:
// import from the module above
use printer::pretty_print_pi_approx;


fn main() {
pretty_print_pi_approx(100_000);
}
  1. Now, where does this new function live? It has its own module:
// Rust will also accept if you implement it right away
mod printer {
// import a function from an external crate (no more extern
declaration required!)
use rust_pilib::monte_carlo_pi;

// internal crates can always be imported using the crate
// prefix
use crate::rounding::round;

pub fn pretty_print_pi_approx(iterations: usize) {
let pi = monte_carlo_pi(iterations);
let places: usize = 2;

println!("Pi is ~ {} and rounded to {} places {}", pi,
places, round(pi, places));
}
}
  1. This module was implemented inline, which is common for tests—but works almost like it was its own file. Looking at the use statements, we are still missing a module, however: rounding. Create a file in the same directory as main.rs and name it rounding.rs. Add this public function and its test to the file:

pub fn round(nr: f32, places: usize) -> f32 {
let multiplier = 10_f32.powi(places as i32);
(nr * multiplier + 0.5).floor() / multiplier
}


#[cfg(test)]
mod tests {
use super::round;

#[test]
fn round_positive() {
assert_eq!(round(3.123456, 2), 3.12);
assert_eq!(round(3.123456, 4), 3.1235);
assert_eq!(round(3.999999, 2), 4.0);
assert_eq!(round(3.0, 2), 3.0);
assert_eq!(round(9.99999, 2), 10.0);
assert_eq!(round(0_f32, 2), 0_f32);
}

#[test]
fn round_negative() {
assert_eq!(round(-3.123456, 2), -3.12);
assert_eq!(round(-3.123456, 4), -3.1235);
assert_eq!(round(-3.999999, 2), -4.0);
assert_eq!(round(-3.0, 2), -3.0);
assert_eq!(round(-9.99999, 2), -10.0);
}
}
  1. So far, the module is ignored by the compiler since it was never declared. Let's do just that and add two lines at the top of main.rs:
// declare the module by its file name
mod rounding;
  1. Lastly, we want to see whether everything worked. cd into the root directory of the pi-estimator project and run cargo run. The output should look similar to this (note that the library crate and dependencies are actually built with pi-estimator):
$ cargo run
Compiling libc v0.2.50
Compiling rand_core v0.4.0
Compiling rand_core v0.3.1
Compiling rand v0.5.6
Compiling rust-pilib v0.1.0 (Rust-Cookbook/Chapter01/rust-pilib)
Compiling pi-estimator v0.1.0 (Rust-Cookbook/Chapter01/pi-
estimator)
Finished dev [unoptimized + debuginfo] target(s) in 4.17s
Running `target/debug/pi-estimator`
Pi is ~ 3.13848 and rounded to 2 places 3.14
  1. Library crates are not the only ones to have tests. Run cargo test to execute the tests in the new pi-estimator project:
$ cargo test
Compiling pi-estimator v0.1.0 (Rust-Cookbook/Chapter01/pi-
estimator)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running target/debug/deps/pi_estimator-1c0d8d523fadde02

running 2 tests
test rounding::tests::round_negative ... ok
test rounding::tests::round_positive ... ok

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

Now, let's go behind the scenes to understand the code better.

How it works...

In this recipe, we explored the relationship between crates and modules. Rust supports several ways of encapsulating code into units, and the 2018 edition has made it a lot easier to do. Seasoned Rust programmers will miss the extern crate declaration(s) at the top of the files, which is nowadays only necessary in special cases. Instead, the crate's contents can be used right away in a use statement. 

In this way, the line between modules and crates is now blurred. However, modules are much simpler to create since they are part of the project and only need to be declared in the root module to be compiled. This declaration is done using the mod statement, which also supports implementation in its body—something that is used a lot in testing. Regardless of the implementation's location, using an external or internal function requires a use statement, often prefixed with crate:: to hint toward its location. 

Alternatively to simple files, a module can also be a directory that contains at least a mod.rs file. This way, large code bases can nest and structure their traits and structs accordingly.

A note on function visibility: Rust's default parameter is module visibility. Hence, a function declared and implemented in a module can only be seen from within that module. Contrary to that, the pub modifier exports the function to outside users. The same goes for properties and functions attached to a struct. 

We've successfully learned how to split our code with crates and modules. Now, let's move on to the next recipe.

 

Writing tests and benchmarks

When we start developing, tests take a backseat more often than not. There are several reasons why this might be necessary at the time, but the inability to set up a testing framework and surroundings is not one of them. Unlike many languages, Rust supports testing right out of the box. This recipe covers how to use these tools.

Although we mostly talk about unit testing here, that is, tests on a function/struct level, the tools remain the same for integration tests. 

Getting ready

Again, this recipe is best worked on in its own project space. Use cargo new testing --lib to create the project. Inside the project directory, create another folder and call it tests.

Additionally, the benchmarks feature is still only available on the nightly branch of Rust. It is required to install the nightly build of Rust: rustup install nightly

How to do it...

Follow these steps to learn more about creating a test suite for your Rust projects:

  1. Once created, a library project already contains a very simple test (probably to encourage you to write more). The cfg(test) and test attributes tell cargo (the test runner) how to deal with the module:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
  1. Before we add further tests, let's add a subject that needs testing. In this case, let's use something interesting: a singly linked list from our other book (Hands-On Data Structures and Algorithms with Rust) made generic. It consists of three parts. First is a node type:
#[derive(Clone)]
struct Node<T> where T: Sized + Clone {
value: T,
next: Link<T>,
}

impl<T> Node<T> where T: Sized + Clone {
fn new(value: T) -> Rc<RefCell<Node<T>>> {
Rc::new(RefCell::new(Node {
value: value,
next: None,
}))
}
}

Second, we have a Link type to make writing easier:

type Link<T> = Option<Rc<RefCell<Node<T>>>>;

The last type is the list complete with functions to add and remove nodes. First, we have the type definition:

#[derive(Clone)]
pub struct List<T> where T: Sized + Clone {
head: Link<T>,
tail: Link<T>,
pub length: usize,
}

In the impl block, we can then specify the operations for the type:

impl<T> List<T> where T: Sized + Clone {
pub fn new_empty() -> List<T> {
List { head: None, tail: None, length: 0 }
}

pub fn append(&mut self, value: T) {
let new = Node::new(value);
match self.tail.take() {
Some(old) => old.borrow_mut().next = Some(new.clone()),
None => self.head = Some(new.clone())
};
self.length += 1;
self.tail = Some(new);
}

pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
if let Some(next) = head.borrow_mut().next.take() {
self.head = Some(next);
} else {
self.tail.take();
}
self.length -= 1;
Rc::try_unwrap(head)
.ok()
.expect("Something is terribly wrong")
.into_inner()
.value
})
}
}
  1. With the list ready to be tested, let's add some tests for each function, starting with a benchmark:

#[cfg(test)]
mod tests {
use super::*;
extern crate test;
use test::Bencher;

#[bench]
fn bench_list_append(b: &mut Bencher) {
let mut list = List::new_empty();
b.iter(|| {
list.append(10);
});
}

Add some more tests for basic list functionality inside the test module:

    #[test]
fn test_list_new_empty() {
let mut list: List<i32> = List::new_empty();
assert_eq!(list.length, 0);
assert_eq!(list.pop(), None);
}

#[test]
fn test_list_append() {
let mut list = List::new_empty();
list.append(1);
list.append(1);
list.append(1);
list.append(1);
list.append(1);
assert_eq!(list.length, 5);
}


#[test]
fn test_list_pop() {
let mut list = List::new_empty();
list.append(1);
list.append(1);
list.append(1);
list.append(1);
list.append(1);
assert_eq!(list.length, 5);
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.length, 0);
assert_eq!(list.pop(), None);
}
}
  1. It's also a good idea to have an integration test that tests the library from end to end. For that, Rust offers a special folder in the project called tests, which can house additional tests that treat the library as a black box. Create and open the tests/list_integration.rs file to add a test that inserts 10,000 items into our list:
use testing::List;

#[test]
fn test_list_insert_10k_items() {
let mut list = List::new_empty();
for _ in 0..10_000 {
list.append(100);
}
assert_eq!(list.length, 10_000);
}
  1. Great, now each function has one test. Try it out by running cargo +nightly test in the testing/ root directory. The result should look like this:
$ cargo test
Compiling testing v0.1.0 (Rust-Cookbook/Chapter01/testing)
Finished dev [unoptimized + debuginfo] target(s) in 0.93s
Running target/debug/deps/testing-a0355a7fb781369f

running 4 tests
test tests::test_list_new_empty ... ok
test tests::test_list_pop ... ok
test tests::test_list_append ... ok
test tests::bench_list_append ... ok

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

Running target/debug/deps/list_integration-77544dc154f309b3

running 1 test
test test_list_insert_10k_items ... ok

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

Doc-tests testing

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  1. To run the benchmark, issue cargo +nightly bench:
cargo +nightly bench
Compiling testing v0.1.0 (Rust-Cookbook/Chapter01/testing)
Finished release [optimized] target(s) in 0.81s
Running target/release/deps/testing-246b46f1969c54dd

running 4 tests
test tests::test_list_append ... ignored
test tests::test_list_new_empty ... ignored
test tests::test_list_pop ... ignored
test tests::bench_list_append ... bench: 78 ns/iter (+/- 238)

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

Now, let's go behind the scenes to understand the code better.

How it works...

Testing frameworks are a third-party library in many programming languages although well-tested code should be the default! By providing a (tiny) testing framework along with a test runner and even a small benchmarking framework (only on nightly as of this writing), the barrier for testing your Rust code is significantly lower. Although there are still some missing features (for example, mocking), the community is working on providing many of these things via external crates. 

After setting everything up in step 1, step 2 creates a singly linked list as the test subject. A singly linked list is a series of the same node types, connected with some sort of pointer. In this recipe, we decided to use the interior mutability pattern, which allows for borrowing mutably at runtime to modify the node it points to. The attached operations (append() and pop()) make use of this pattern. Step 3 then creates the tests that we can use to verify that our code does what we think it should. These tests cover the basic workings of the list: create an empty list, append a few items, and remove them again using pop

Tests can be failed using a variety of assert! macros. They cover equals (assert_eq!), not equals (assert_ne!), Boolean conditions (assert!), and non-release mode compilation only (debug_assert!). With these available and attributes such as #[should_panic], there is no case that cannot be covered. Additionally, this great Rust book offers an interesting read as well: https://doc.rust-lang.org/book/ch11-01-writing-tests.html.

Step 4 adds a special integration test in a separate file. This restricts programmers to think like the user of the crate, without access to internal modules and functions that can be available in the nested tests module. As a simple test, we insert 10,000 items into the list to see whether it can handle the volume. 

The +nightly parameter instructs cargo to use the nightly toolchain for this command.

Only in step 5 are we ready to run the benchmarks using cargo +nightly test, but tests are not automatically benchmarked. On top of that, benchmarks (cargo +nightly bench) compile the code using --release flags, thereby adding several optimizations that could lead to different outcomes from cargo +nightly test (including a headache for debugging those).

Step 6 shows the output of the benchmarking harness with nanosecond precision for each loop execution (and the standard deviation). Whenever doing any kind of performance optimization, have a benchmark ready to show that it actually worked!

Other nice things that the Rust documentation tool adds to testing are doctests. These are snippets that are compiled and executed as well as rendered as documentation. We were so delighted, we gave it its own recipe! So, let's move on to the next recipe.

 

Documenting your code

Documentation is an important part of software engineering. Instead of simply writing up some functions and chaining them together on a hunch, we like to promote writing reusable and readable code. Part of this is also writing sensible documentation—which, in ideal cases, can be rendered into other formats such as HTML or PDF. As many languages do by default, Rust provides a tool and language support as well: rustdoc.

Getting ready

Failing our high standards of software engineering, we did not document the code from the last recipe! To change that, let's load a project with code to be documented (such as the previous recipe, Writing tests and benchmarks) into an editor.

How to do it...

Compile your code comments to a shiny HTML in just a few steps:

  1. Rust's docstrings (strings that explicitly are documentation to be rendered) are denoted by /// (instead of the regular //). Within these sections, markdown—a shorthand language for HTMLcan be used to create full documentation. Let's add the following before the List<T> declaration: 
/// 
/// A singly-linked list, with nodes allocated on the heap using
///`Rc`s and `RefCell`s. Here's an image illustrating a linked list:
///
///
/// ![](https://upload.wikimedia.org/wikipedia/commons/6/6d/Singly-
///linked-list.svg)
///
/// *Found on https://en.wikipedia.org/wiki/Linked_list*
///
/// # Usage
///
/// ```
/// let list = List::new_empty();
/// ```
///
#[derive(Clone)]
pub struct List<T> where T: Sized + Clone {
[...]
  1. This makes the code a lot more verbose, but is this worth it? Let's see with cargo doc, a subcommand that runs rustdoc on the code and outputs HTML in the target/doc directory of the project. When opened in a browser, the target/doc/testing/index.html page shows the following (and more):

Replace testing with the name of your project!
  1. Great, let's add more documentation in the code. There are even special sections that are recognized by the compiler (by convention):
    ///
/// Appends a node to the list at the end.
///
///
/// # Panics
///
/// This never panics (probably).
///
/// # Safety
///
/// No unsafe code was used.
///
/// # Example
///
/// ```
/// use testing::List;
///
/// let mut list = List::new_empty();
/// list.append(10);
/// ```
///
pub fn append(&mut self, value: T) {
[...]
  1. The /// comments add documentation for expressions that follow it. This is going to be a problem for modules: should we put the documentation outside of the current module? No. Not only will this make the maintainers confused, but it also has a limit. Let's use //! to document the module from within:
//!
//! A simple singly-linked list for the Rust-Cookbook by Packt
//! Publishing.
//!
//! Recipes covered in this module:
//! - Documenting your code
//! - Testing your documentation
//! - Writing tests and benchmarks
//!
  1. A quick cargo doc run reveals whether it worked:

  1. While there is some benefit in having similar-looking documentation in any Rust project, corporate marketing often likes to have things such as logos or a custom favicon to stand out. rustdoc supports that with attributes on the module level—they can be added right below the module documentation (note: this is the logo of my Rust blog, https://blog.x5ff.xyz):
#![doc(html_logo_url = "https://blog.x5ff.xyz/img/main/logo.png")]
  1. To see whether it worked, let's run cargo doc again:

Now, let's go behind the scenes to understand the code better.

How it works...

Markdown is a great language that allows for creating formatted documentation quickly. However, feature support is typically tricky, so check out Rust's RFC for supported formatting (https://github.com/rust-lang/rfcs/blob/master/text/0505-api-comment-conventions.md) to find out whether some more advanced statements can be used. In general, writing documentation is dreaded by most developers, which is why it's very important to make it as simple and effortless as possible. The /// pattern is quite common and has been expanded in Rust so that the documentation can apply to the code that follows (///) or that contains it (//!). Examples can be seen in step 1 and step 4.

The approach the Rust project chose allows for a few lines explaining the (public) function, and then the rustdoc compiler (invoked in step 2 with cargo doc) does the rest: exposing public members, cross-linking, listing all of the available types and modules, and much more. While the output is fully customizable (step 6), the default is already visually quite appealing (we think). 

By default, cargo doc builds the documentation for the entire project—including dependencies. 

Special sections (step 3) add another dimension to the documentation output: they allow for IDEs or editors to make some sense of the provided information and highlight, for example, that (and when) a function may panic. The examples section in your newly generated documentation will even compile and run code in the form of doctests (see the Testing your documentation recipe) so you will be notified when your examples become invalid.

The rustdoc output is also independent of a web server, which means that it can be used wherever static hosting is supported. In fact, the Rust project builds and serves every crate's documentation that is hosted on https://crates.io, on https://docs.rs

Now that we can create documentation successfully, we should move on to the next recipe.

 

Testing your documentation

Out-of-date documentation and examples that aren't working as promised are an unfortunate truth of many technologies. However, these examples can be valuable (black box) regression tests to make sure that we didn't break anything while improving the code, so how can they be used as such? Rust's documentation strings (///) can include executable code snippets—and they can be seen all over the place on https://www.rust-lang.org/learn!

Getting ready

We'll continue to improve the linked list from a previous recipe but focus some more on the documentation. However, the added code will work in any project, so pick one that you want to add documentation to and open it in your favorite editor. 

How to do it...

Here are the steps for this recipe:

  1. Find a function or struct (or module) to add a documentation string, for example, the new_empty() function of List<T>:
    ///
/// Creates a new empty list.
///
///
pub fn new_empty() -> List<T> {
...
  1. Use the special (H1) section # Example to provide a cue for the compiler to run any snippet contained in that section:
    ///
/// Creates a new empty list.
///
///
/// # Example
  1. Now let's add a code example. Since doctests are considered black box tests, we import the struct (only if it's public, of course) and show what we want to show:
    ///
/// Creates a new empty list.
///
///
/// # Example
///
/// ```
/// use testing::List;
///
/// let mut list: List<i32> = List::new_empty();
/// ```
///
  1. With that ready, let's see whether the tests work: run cargo +nightly test in the project's root directory. You can see that we cheated a little bit and added tests to the other functions as well:
$ cargo +nightly test
Compiling testing v0.1.0 (Rust-Cookbook/Chapter01/testing)
Finished dev [unoptimized + debuginfo] target(s) in 0.86s
Running target/debug/deps/testing-a0355a7fb781369f

running 6 tests
[...]
Doc-tests testing

running 4 tests
test src/lib.rs - List (line 44) ... ok
test src/lib.rs - List<T>::new_empty (line 70) ... ok
test src/lib.rs - List<T>::append (line 94) ... ok
test src/lib.rs - List<T>::pop (line 121) ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  1. The code obviously has been augmented with several examples that have been run in this case—is that always what we want? Sometimes, it's all about the output, and adding all of the required imports for the test to successfully run is a pain. Hence, there are options to add to the fenced area (``` inside the fence ```), and ignore will neither compile nor run the code:
/// 
/// A singly-linked list, with nodes allocated on the heap using `Rc`s and `RefCell`s. Here's an image illustrating a linked list:
///
///
/// ![](https://upload.wikimedia.org/wikipedia/commons/6/6d/Singly-linked-list.svg)
///
/// *Found on https://en.wikipedia.org/wiki/Linked_list*
///
/// # Example
///
/// ```ignore
///
/// let list = List::new_empty();
/// ```
///
#[derive(Clone)]
pub struct List<T> where T: Sized + Clone {
[...]

  1. By running cargo test again, we see the changes reflected in the output:
$ cargo test
[...]
Doc-tests testing

running 4 tests
test src/lib.rs - List (line 46) ... ignored
test src/lib.rs - List<T>::append (line 94) ... ok
test src/lib.rs - List<T>::new_empty (line 70) ... ok
test src/lib.rs - List<T>::pop (line 121) ... ok

test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
  1. Let's check the HTML output as well: run cargo doc to generate a target/doc/ directory containing all of the CSS/HTML/JavaScript/... required to show the documentation in a local browser. Open target/doc/testing/index.html with your favorite browser:

Note: Replace testing with the name of your project.
  1. Let's remove the ugly use statement at the top of the snippet. At that point, it doubles the lines displayed without adding anything—and rustdoc provides a simple way to do that, too. Add # in front of the offending line:
    ///
/// Creates a new empty list.
///
///
/// # Example
///
/// ```
/// # use testing::List;
/// let list: List<i32> = List::new_empty();
/// ```
///
pub fn new_empty() -> List<T> {
[...]
  1. Lastly, there are additional ways to configure the testing behavior of doctests. In this case, let's change warnings to errors by denying the warning while ignoring (allowing) unused variables:  
#![doc(html_logo_url = "https://blog.x5ff.xyz/img/main/logo.png",
test(no_crate_inject, attr(allow(unused_variables),
deny(warnings))))]
  1. One last time, let's check whether the output is what we expect and run cargo doc:

Now, let's see whether we can find out more about how the code works.

How it works...

Rust's documentation is very versatile and allows for variations on doctests that would not be possible to cover in a single recipe. However, the documentation of these tools is also excellent, so, for more details, check out https://doc.rust-lang.org/rustdoc/documentation-tests.html

What we covered in this recipe is a great way to document structs and functions in your code by adding examples that will be compiled and run on every test run. Not only will these be helpful for your readers and regression testing, but they also require you to think about how the code works as a black box. These tests are executed whenever code (``` in a fence ```) is encountered in the Example section of the documentation. In step 2 and step 3, we create these examples and see the result in step 4 and step 10.  

If you are now wondering how some documentation can show a fraction of the code required while it is supposed to be run, step 8 shows the resolution to this riddle: # can hide individual lines while executing them. However, sometimes the code is not executed at all, as step 5 shows. We can declare a section as ignore and this code won't be run (without any visual indication in the output).  

Furthermore, these tests can fail just like any other test by panicking (which can be allowed as well) or falling through an assert! macro. All in all, by hiding away boilerplate or other non-essential code, the reader can focus on the important bits, while the test still covers everything. 

We've successfully tested our documentation—we can sleep easy and move on to the next recipe.

 

Sharing code among types

An unusual feature of the Rust programming language is the decision to use traits over interfaces. The latter is very common across modern object-oriented languages and unifies the API of a class (or similar) to the caller, making it possible to switch the entire implementation without the caller's knowledge. In Rust, the separation is a bit different: traits are more akin to abstract classes since they provide the API aspect as well as default implementations. struct can implement various traits, thereby offering the same behavior with other structs that implement the same traits. 

How to do it...

Let's go through the following steps:

  1. Use cargo to create a new project, cargo new traits --lib, or clone it from this book's GitHub repository (https://github.com/PacktPublishing/Rust-Programming-Cookbook). Use Visual Studio Code and Terminal to open the project's directory.
  2. Implement a simple configuration management service. To do that, we need some structs to work with:
use std::io::{Read, Write};

///
/// Configuration for our application
///
pub struct Config {
values: Vec<(String, String)>
}

///
/// A service for managing a configuration
///
pub struct KeyValueConfigService {}

Additionally, some constructors make them easier to use:

// Impls

impl Config {
pub fn new(values: Vec<(String, String)>) -> Config {
Config { values: values }
}
}

impl KeyValueConfigService {
pub fn new() -> KeyValueConfigService {
KeyValueConfigService { }
}
}
  1. To use a unified interface with other potential implementations, we have some traits to share the interface:
///
/// Provides a get() function to return values associated with
/// the specified key.
///
pub trait ValueGetter {
fn get(&self, s: &str) -> Option<String>;
}

///
/// Write a config
///
pub trait ConfigWriter {
fn write(&self, config: Config, to: &mut impl Write) -> std::io::Result<()>;
}

///
/// Read a config
///
pub trait ConfigReader {
fn read(&self, from: &mut impl Read) -> std::io::Result<Config>;
}
  1. Rust demands its own implementation block for each trait:
impl ConfigWriter for KeyValueConfigService {
fn write(&self, config: Config, mut to: &mut impl Write) -> std::io::Result<()> {
for v in config.values {
writeln!(&mut to, "{0}={1}", v.0, v.1)?;
}
Ok(())
}
}

impl ConfigReader for KeyValueConfigService {
fn read(&self, from: &mut impl Read) -> std::io::Result<Config> {
let mut buffer = String::new();
from.read_to_string(&mut buffer)?;

// chain iterators together and collect the results
let values: Vec<(String, String)> = buffer
.split_terminator("\n") // split
.map(|line| line.trim()) // remove whitespace
.filter(|line| { // filter invalid lines
let pos = line.find("=")
.unwrap_or(0);
pos > 0 && pos < line.len() - 1
})
.map(|line| { // create a tuple from a line
let parts = line.split("=")
.collect::<Vec<&str>>();
(parts[0].to_string(), parts[1].to_string())
})
.collect(); // transform it into a vector
Ok(Config::new(values))
}
}

impl ValueGetter for Config {
fn get(&self, s: &str) -> Option<String> {
self.values.iter()
.find_map(|tuple| if &tuple.0 == s {
Some(tuple.1.clone())
} else {
None
})
}
}
  1. Next, we need some tests to show it in action. To cover some basics, let's add best-case unit tests:
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;

#[test]
fn config_get_value() {
let config = Config::new(vec![("hello".to_string(),
"world".to_string())]);
assert_eq!(config.get("hello"), Some("world".to_string()));
assert_eq!(config.get("HELLO"), None);
}


#[test]
fn keyvalueconfigservice_write_config() {
let config = Config::new(vec![("hello".to_string(),
"world".to_string())]);

let service = KeyValueConfigService::new();
let mut target = vec![];
assert!(service.write(config, &mut target).is_ok());

assert_eq!(String::from_utf8(target).unwrap(),
"hello=world\n".to_string());
}

#[test]
fn keyvalueconfigservice_read_config() {

let service = KeyValueConfigService::new();
let readable = &format!("{}\n{}", "hello=world",
"a=b").into_bytes();

let config = service.read(&mut Cursor::new(readable))
.expect("Couldn't read from the vector");

assert_eq!(config.values, vec![
("hello".to_string(), "world".to_string()),
("a".to_string(), "b".to_string())]);
}
}
  1. Lastly, we run cargo test and see that everything works out:
$ cargo test
Compiling traits v0.1.0 (Rust-Cookbook/Chapter01/traits)
Finished dev [unoptimized + debuginfo] target(s) in 0.92s
Running target/debug/deps/traits-e1d367b025654a89

running 3 tests
test tests::config_get_value ... ok
test tests::keyvalueconfigservice_write_config ... ok
test tests::keyvalueconfigservice_read_config ... ok

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

Doc-tests traits

running 0 tests

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

Now, let's go behind the scenes to understand the code better.

How it works...

Using traits instead of interfaces and other object-oriented constructs has many implications for the general architecture. In fact, common architectural thinking will likely lead to more complex and verbose code that may perform worse on top of that! Let's examine popular object-oriented principles from the Gang of Four's book, Design Patterns (1994):

  • Program to an interface not to an implementation: This principle requires some thinking in Rust. With the 2018 edition, functions can accept an impl MyTrait parameter, where earlier versions had to use Box<MyTrait> or o: T and later where T: MyTrait, all of which have their own issues. It's a trade-off for every project: either less complex abstractions with the concrete type or more generics and other complexity for cleaner encapsulation. 
  • Favor object composition over class inheritance: While this only applies to some extent (there is no inheritance in Rust), object composition is still something that seems like a good idea. Add trait type properties to your struct instead of the actual type. However, unless it's a boxed trait (that is, slower dynamic dispatch), there is no way for the compiler to know exactly the size it should reserve—a type instance could have 10 times the size of the trait from other things. Therefore, a reference is required. Unfortunately, though, that introduces explicit lifetimes—making the code a lot more verbose and complex to handle.

Rust clearly favors splitting off behavior from data, where the former goes into a trait and the latter remains with the original struct. In this recipe, KeyValueConfigService did not have to manage any data—its task was to read and write Config instances.

After creating these structs in step 2, we created the behavior traits in step 3. There, we split the tasks off into two individual traits to keep them small and manageable. Anything can implement these traits and thereby acquire the capabilities of writing or reading config files or retrieving a specific value by its key. 

We kept the functions on the trait generic as well to allow for easy unit testing (we can use Vec<T> instead of faking files). Using Rust's impl trait feature, we only care about the fact that std::io::Read and std::io::Write have been implemented by whatever is passed in.

Step 4 implements the traits in an individual impl block for the structs. The ConfigReader strategy is naive: split into lines, split those lines at the first = character, and declare the left- and right-hand parts key and value respectively. The ValueGetter implementation then walks through the key-value pairs to find the requested key. We preferred Vec with String tuples here for simplicity, for example, HashMap can improve performance substantially.

The tests implemented in step 5 provide an overview of how the system works and how we seamlessly use the types by the traits they implement. Vec doubles as a read/write stream, no type-casting required. To make sure the tests actually run through, we run cargo test in step 6

 After this lesson on structuring code, let's move on to the next recipe.

 

Sequence types in Rust

Sequences are supported in many forms in Rust. The regular array is strictly implemented: it has to be defined at compile time (using literals) and be of a single data type, and cannot change in size. Tuples can have members of different types, but cannot change in size either. Vec<T> is a generic sequence type (of whatever you define as type T) that provides dynamic resizing—but T can only be of a single type. All in all, each of them has its purpose and, in this recipe, we will explore each.  

How to do it...

The steps for this recipe are as follows:

  1. Use cargo to create a new project, cargo new sequences --lib, or clone it from this book's GitHub repository (https://github.com/PacktPublishing/Rust-Programming-Cookbook). Use Visual Studio Code and Terminal to open the project's directory.
  2. With the test module ready, let's start with arrays. Arrays in Rust have a familiar syntax but they follow a stricter definition. We can try out various abilities of the Rust array in a test:
    #[test]
fn exploring_arrays() {
let mut arr: [usize; 3] = [0; 3];
assert_eq!(arr, [0, 0, 0]);

let arr2: [usize; 5] = [1,2,3,4,5];
assert_eq!(arr2, [1,2,3,4,5]);

arr[0] = 1;
assert_eq!(arr, [1, 0, 0]);
assert_eq!(arr[0], 1);
assert_eq!(mem::size_of_val(&arr), mem::size_of::<usize>()
* 3);
}
  1. Users of more recent programming languages and data science/math environments will also be familiar with the tuple, a fixed-size variable type collection. Add a test for working with tuples:
    struct Point(f32, f32);

#[test]
fn exploring_tuples() {
let mut my_tuple: (i32, usize, f32) = (10, 0, -3.42);

assert_eq!(my_tuple.0, 10);
assert_eq!(my_tuple.1, 0);
assert_eq!(my_tuple.2, -3.42);

my_tuple.0 = 100;
assert_eq!(my_tuple.0, 100);

let (_val1, _val2, _val3) = my_tuple;

let point = Point(1.2, 2.1);
assert_eq!(point.0, 1.2);
assert_eq!(point.1, 2.1);
}
  1. As the last collection, the vector is the basis for all of the other quick and expandable data types. Create the following test with several assertions that show how to use the vec! macro and the vector's memory usage:
    use std::mem;

#[test]
fn exploring_vec() {
assert_eq!(vec![0; 3], [0, 0, 0]);
let mut v: Vec<i32> = vec![];

assert_eq!(mem::size_of::<Vec<i32>>(),
mem::size_of::<usize>
() * 3);

assert_eq!(mem::size_of_val(&*v), 0);

v.push(10);

assert_eq!(mem::size_of::<Vec<i32>>(),
mem::size_of::<i32>() * 6);

The remainder of the test shows how to modify and read the vector:

        assert_eq!(v[0], 10);

v.insert(0, 11);
v.push(12);
assert_eq!(v, [11, 10, 12]);
assert!(!v.is_empty());

assert_eq!(v.swap_remove(0), 11);
assert_eq!(v, [12, 10]);

assert_eq!(v.pop(), Some(10));
assert_eq!(v, [12]);

assert_eq!(v.remove(0), 12);

v.shrink_to_fit();
assert_eq!(mem::size_of_val(&*v), 0);
}

  1. Run cargo test to see the working tests run:
$ cargo test
Compiling sequences v0.1.0 (Rust-Cookbook/Chapter01/sequences)
Finished dev [unoptimized + debuginfo] target(s) in 1.28s
Running target/debug/deps/sequences-f931e7184f2b4f3d

running 3 tests
test tests::exploring_arrays ... ok
test tests::exploring_tuples ... ok
test tests::exploring_vec ... ok

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

Doc-tests sequences

running 0 tests

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

Now, let's go behind the scenes to understand the code better.

How it works...

Sequence types are compound types that allocate a continuous part of the memory for faster and easier access. Vec<T> creates a simple, heap-allocated version of an array that grows (and shrinks) dynamically (step 4). 

The original array (step 2) allocates memory on the stack and has to have a known size at compile time, which is a significant factor in using it. Both can be iterated and viewed using slices (https://doc.rust-lang.org/book/ch04-03-slices.html).

Tuples (step 3) are a different beast since they don't lend themselves to slices and are more a group of variables that have a semantic relationship—like a point in a two-dimensional space. Another use case is to return more than one variable to the caller of a function without the use of an additional struct or misusing a collection type.

Sequences in Rust are special because of the low overhead they produce. The size of Vec<T> is a pointer to an n * size of T memory on the heap, along with the size of the allocated memory, and how much of that is used. For arrays, the capacity is the current size (which the compiler can fill in during compilation), and tuples are more or less syntactic sugar on top of three distinct variables. Each of the three types provides convenience functions to change the contents—and, in the case of Vec<T>, the size of the collection. We recommend taking a close look at the tests and their comments to find out more about each type.  

We have covered the basics of sequences in Rust, so let's move on to the next recipe.

 

Debugging Rust

Debugging has been a notoriously difficult topic in Rust, but still, it pales in comparison to Visual Studio debugging or IntelliJ IDEA's (https://www.jetbrains.com/idea/) capabilities in the Java world. However, debugging capabilities go beyond simple println! statements nowadays. 

Getting ready

Debugging Rust is available via an additional extension in Visual Studio Code. Install it by running ext install vadimcn.vscode-lldb in the command window (Ctrl + P/cmd + P).

On Windows, debugging is limited thanks to its incomplete LLVM support. However, the extension will prompt you to automatically install several things. Additionally, install Python 3.6 and add it to %PATH%. With these dependencies installed, it worked well for us (in March 2019). 

Read more at https://github.com/vadimcn/vscode-lldb/wiki/Setup.

How to do it...

Execute the following steps for this recipe:

  1. Create a new binary project to debug: cargo new debug-me. Open this project in Visual Studio Code with the new extension loaded.
  2. Before anything can happen, Visual Studio Code needs a launch configuration to recognize Rust's LLVM output. First, let's create this launch configuration; for that, add a .vscode directory containing a launch.json file to the project directory. This can be autogenerated, so make sure that launch.json contains the following:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'debug-me'",
"cargo": {
"args": [
"build",
"--bin=debug-me",
"--package=debug-me"
],
"filter": {
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'debug-me'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=debug-me",
"--package=debug-me"
],
"filter": {
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
  1. Now, let's open src/main.rs and add some code to debug:
struct MyStruct {
prop: usize,
}

struct Point(f32, f32);

fn main() {
let a = 42;
let b = vec![0, 0, 0, 100];
let c = [1, 2, 3, 4, 5];
let d = 0x5ff;
let e = MyStruct { prop: 10 };
let p = Point(3.14, 3.14);

println!("Hello, world!");
}

  1. Save and add a breakpoint in VS Code's user interface. Click left of the line numbers and a red dot should appear there. This is a breakpoint: 

  1. Having set a breakpoint, we expect the program to pause there and give us some insights into the current memory layout, that is, the state of any variables at that particular point in time. Run the debug launch configuration with F5 (or Debug | Start Debugging). The window configuration should change slightly and a panel on the left-hand side of the window shows local variables (among other things):

  1. Using the small control panel on top, you can then control the execution flow and watch the stack and memory on the left change accordingly. Note also the difference between an array and a (heap-allocated) vector!

Now, let's go behind the scenes to understand the code better.

How it works...

Rust is built on the LLVM compiler toolkit that comes with a range of features out of the box. When a Rust program compiles, it only gets translated into an intermediate language, from which the LLVM compiler creates native bytecode.

This is also the reason why debugging can work in this case—it builds on the LLVM debug symbols. While it clearly lacks the convenience of modern IDEs, it's a large step forward and allows users to inspect types. Future development of the tools will hopefully improve this situation as well; for now, the general debugger, GDB (https://www.gnu.org/software/gdb/), handles most of the cases where debug symbols are compiled into the program. The configuration for connecting the debugger with the code in the IDE can be found in step 2 and, by setting the breakpoint in step 4, it can track the relationship between lines of code and output. With the default setting to compile to debug, the debugger can then stop at this exact point. While it's not perfect (on the UX side), its capabilities are amazing. 

Even this simple connection to a (UX-wise) very basic debugger can have great benefits for developers and represents a huge step up from println!() statements to inspect the current value of a variable. 

We hope that you can use the debugger's capabilities in the remainder of this book. With this knowledge, you can now move on to the next chapter.

About the Author

  • Claus Matzinger

    Claus Matzinger is a software engineer with a very diverse background. After working in a small company maintaining code for embedded devices, he joined a large corporation to work on legacy Smalltalk applications. This led to a great interest in programming languages early on, and Claus became the CTO for a health games start-up based on Scala technology. Since then, Claus' roles have shifted toward customer-facing roles in the IoT database technology start-up, Crate IO (creators of CrateDB), and, most recently, Microsoft. There, he hosts a podcast, writes code together with customers, and blogs about the solutions arising from these engagements. For more than 5 years, Claus has been implementing software to help customers innovate, achieve, and maintain success.

    Browse publications by this author

Latest Reviews

(2 reviews total)
Competent writers, well edited books, aimed at most different practices and needs,
Love it :)

Recommended For You

Book Title
Unlock this full book with a FREE 10-day trial
Start Free Trial