Rust 2018: Productivity

The Rust Standard Library and tooling have improved a lot over the years. Since February 2018, the Rust ecosystem has become quite wide and multifaceted. Four domain working groups have been created, each covering one of the main application areas. These areas were already fairly mature, but this development allowed them to improve even further. In the coming years, we will see the introduction of other domain working groups as well.

It's not an easy task to develop a high-quality and cost-effective application, even after learning a language as a developer. To avoid reinventing the (presumably low-quality) wheel, you as a developer should use a high-quality framework or some high-quality libraries that cover the kind of application you are going to develop.

The purpose of this book is to guide you as a developer to choose the best open source Rust libraries available for developing software. This book covers several typical domains, each using different libraries. Because some non-standard libraries are useful in a number of different domains, it would be quite limiting to present them as confined to a single domain.

In this chapter, you will learn about the following topics:

  • Understanding the different editions of Rust
  • Understanding the most important recent improvements made to Rust
  • Understanding domain working groups
  • Understanding the kind of projects that we will cover in this book
  • An introduction to some useful Rust libraries

Technical requirements

To follow this book, you will need to have access to a computer on which a recent Rust system is installed. Any release since version 1.31 is okay. Some optional libraries will be listed for some specific projects later on.

Any cited source code and additional examples can (and should) be downloaded from the repository: https://github.com/PacktPublishing/Creative-Projects-for-Rust-Programmers.

Understanding the different editions of Rust

On December 6, 2018, a very important version of the Rust language, its compiler, and its standard library was released: stable version 1.31. This version has been defined as the 2018 edition, meaning it is a milestone that will be used as a reference for the years to come.

Before this, there was another version, 1.0, which was defined as the 2015 edition. This edition was characterized by the word stability. Up until version 1.0, every version of the compiler applied breaking changes to the language or to the standard library, forcing the developers to apply sweeping changes to their code base. From version 1.0, efforts have been made to ensure that any future version of the compiler can correctly compile any code written for version 1.0 or successive versions. This is called backward compatibility.

However, many features were applied to the language and to the standard library before the release of the 2018 edition. Many new libraries used these new features, meaning that these libraries could not be used by older compilers. For this reason, there was a need to tag a specific version of Rust as aimed at being used with newer libraries. This was the main reason for the 2018 edition.

Some of the features added to the language are marked as for the 2015 edition, while others are marked as for the 2018 edition. The features for the 2015 edition are just small improvements, while the features for the 2018 edition are more in-depth changes. Developers must mark their crates as for the 2018 edition in order to use the features that are specific to the 2018 edition.

In addition, although the 2015 edition marked a stable milestone for the language and the standard library, the command-line tools were not actually stabilized; they were still quite immature. In the three and a half years from May 2015 to December 2018, the main official command-line tools have matured, and the language has also been improved to allow more efficient coding. The 2018 edition can be characterized by the word productivity.

The following table shows a timeline of the features stabilized in the language, the standard library, and the tooling:

2015 May: 2015 edition August: Parallel compilation on multi-core CPUs
2016 April: Microsoft C compiler format supported May: Ability to capture panics September: Improved compiler error messages November: The ? operator December: The rustup command
2017 February: Custom derive attributes March: The cargo check command July: The union keyword August: Associated constants November: The ? operator with Option
2018 February:
  • The formation of four Domain Working Groups.
  • The rustfmt program
May:
  • The Rust Programming Language Second Edition.
  • The impl Trait language feature.
  • main can return a Result.
  • Inclusive ranges with ..=
  • The i128 and u128 native types.
  • Improved patterns for match
June:
  • The SIMD library feature
  • The dyn Trait language feature
August: Custom global allocator September:
  • The cargo fix command
  • The cargo clippy command
October:
  • Procedural macros
  • Changes to the module system and the use statement
  • Raw identifiers
  • no_std applications
December:
  • The 2018 edition
  • Non-lexical lifetimes
  • The const fn language feature
  • The new https://www.rust-lang.org/website
  • try, async, and await are reserved words

Many improvements have been applied since the 2015 edition. More information can be found in the official documentation (https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html). The most important improvements are listed as follows:

  • A new official tutorial book, available free online (https://doc.rust-lang.org/book/), or printed on paper (The Rust Programming Language by Steve Klabnik and Carol Nichols).
  • A revamped official website.
  • The formation of four domain working groups, which are open committees to design the future of the ecosystem in four key areas:
    • Networking: Designing the new asynchronous paradigm around a concept of delayed computation, named future, as it is already done in other languages, such as C++, C#, and JavaScript (with promises).
    • Command-line applications: Designing some standard libraries to support any non-graphical, non-embedded applications.
    • WebAssembly: Designing tools and libraries to build applications to be run inside web browsers.
    • Embedded software: Designing tools and libraries to build applications to be run on bare-metal systems or on strictly constrained hardware.
  • We witnessed some good improvements to the language:
    • Non-lexical lifetimes; any bindings that are no longer used are considered dead. For example, now this program is allowed:
fn main() {
let mut _a = 7;
let _ref_to_a = &_a;
_a = 9;
}
In this code, the object bound to the variable _a is borrowed by the variable _ref_to_a in the second statement. Prior to the introduction of non-lexical lifetimes, such bindings would last till the end of the scope, and so the last statement would have been illegal because it tries to change that object through binding _a when it is still borrowed to variable _ref_to_a. Now, because variable _ref_to_a is no longer used, its lifetime ceases in the same line it is declared, and so, in the last statement, variable _a is again free to change its own object.
    • The Impl Trait feature, which allows functions to return unspecified types, such as closures.
    • The i128 and u128 native types.
    • Some other reserved keywords such as try, async, and await.
    • The ? operator, usable even in the main function, because now it can return Result. The following program is an example of the main function returning a Result:
fn main() -> Result<(), String> {
Err("Hi".to_string())
}

It can succeed, by returning the usual empty tuple or fail by returning the type you specify. In this case, it was String. The following program is an example using the ? operator used in the main function:

fn main() -> Result<(), usize> {
let array = [12, 19, 27];
let found = array.binary_search(&19)?;
println!("Found {}", found);
let found = array.binary_search(&20)?;
println!("Found {}", found);
Ok(())
}

This program will print Found 1 on the standard output stream, meaning that the number 19 has been found at position 1, and it will print Error: 2 on the standard error stream, meaning that the number 20 hasn't been found, but that it should be inserted at position 2.

    • Procedural macros, which allow a kind of meta-programming, manipulating source code to generate Rust code at compile time.
    • More powerful and more ergonomic pattern matching in match expressions.
  • And also some improvements to the standard tooling:
    • The rustup program, which allows users to easily choose the default compiler target or to update the toolchain.
    • The rustfix program, which converts a 2015 edition project to a 2018 edition project.
    • The Clippy program, which checks for non-idiomatic syntax, and suggests changes to code for better maintainability.
    • Faster compilation speed, in particular, if just a syntax check is required.
    • The Rust Language Server (RLS) program, which is currently still unstable, but which allows IDEs and programmable editors to spot syntax errors, and to suggest allowed operations.

Rust is still evolving as a language, like any other programming language. The following areas are still left to be improved:

  • The IDE tools, including a language interpreter (REPL) and a graphical debugger
  • Libraries and tools to support bare-metal and real-time software development
  • Application-level frameworks and libraries for the main application areas

This book will focus primarily on the third point on this list.

The projects

When we write a real-world application, the Rust language and its standard library are not sufficient. Application frameworks are needed for particular kinds of applications, such as GUI apps, web apps, or games.

Of course, if you use a good-quality and comprehensive library, you can reduce the number of lines of code that you need to write. Using a library also offers the following two advantages:

  • The overall design is improved, particularly if you are using a framework (since it imposes an architecture on your app) as it will be created by knowledgeable engineers and time-tested by a number of users.
  • The number of bugs will be reduced because it will have undergone more thorough testing than that which you are likely to be able to apply.

There are actually many Rust libraries, also known as crates, but most are low-quality or quite narrow in their range of applications. This book will look at the best quality and most complete libraries for some typical application areas of the Rust language.

The application areas are as follows:

  • Web apps: There are various popular technologies, including the following:
    • The REST web service (backend only)
    • An event-driven web client (frontend only)
    • A full web app (full-stack)
    • A web game (frontend only)
  • Games: When I saygames, I'm not referring to anything that is entertaining. I am referring to a graphical application where a continuous animation is shown, as opposed to event-driven graphical applications that do nothing until an event occurs, such as the user pressing a key, moving the mouse, or some data arriving from a connection. As well as games for the web browser, there are also games for desktop and laptop computers, for video game consoles, and for mobile devices. However, video game consoles and mobile devices are not yet that well supported by Rust, so we will only be looking at games for desktop and laptop computers in this book.
  • Language interpreters: There are two kinds of languages that can be interpreted. Both arecovered in this book:
    • Text: Like a programming language, a markup language, or a machine command language
    • Binary: Like the machine language of a computer to be emulated, or the intermediate bytecode of a programming language.
  • C-language-callable libraries: This is an important use case of Rust: to develop a library to be invoked by another application, typically written in a higher-level language. Rust cannot assume that other languages can invoke the Rust code, but it can assume that they can invoke the C-language code. We will look at how to build a library that can be invoked as if it were written in C. One particularly challenging case is to build a module for the Linux operating system, which notoriously has to be written in C.

Most applications read and write data to and from a file, or a communication channel, or a database. In the next chapter, we will be looking at various different techniques that will be useful for all the other projects.

Other application areas have not been listed here as they are either not used much in Rust, they are still immature, or they are still in a state of flux. The libraries available for these immature areas will be completely different in a couple of years. These areas include software for micro-controllers, or other real-time or low-resource systems, and also software for mobile or wearable systems.

Working through the examples in this book

To follow the examples in the book, you should download all the examples from the online repository:https://github.com/PacktPublishing/Creative-Projects-for-Rust-Programmers. This repository contains a sub-folder for each chapter of the book and a sub-sub-folder for any project in a chapter.

For example, to run the use_rand project in this chapter, you should go to the Chapter01/use_randfolder and typecargo run. Notice that the most important files of any project are cargo.toml and src/main.rs, so you should always take a look at them first.

Exploring some utility crates

Before moving on to looking at how to use the most complex crates, let's take a look at some basic Rust crates. These are not a part of the standard library, but they are useful in many different kinds of projects. They should be known by all Rust developers since they are of general applicability.

Pseudo-random number generators – the rand crate

The ability to generate pseudo-random numbers is needed for several kinds of applications, especially for games. The rand crate is rather complex, but its basic usage is shown in the following example (named use_rand):

// Declare basic functions for pseudo-random number generators.
use rand::prelude::*;

fn main() {
// Create a pseudo-Random Number Generator for the current thread
let mut rng = thread_rng();

// Print an integer number
// between 0 (included) and 20 (excluded).
println!("{}", rng.gen_range(0, 20));

// Print a floating-point number
// between 0 (included) and 1 (excluded).
println!("{}", rng.gen::<f64>());

// Generate a Boolean.
println!("{}", if rng.gen() { "Heads" } else { "Tails" });
}

First, you create a pseudo-random number generator object. Then, you call several methods on this object. Any generator must be mutable because any generation modifies the state of the generator.

The gen_range method generates an integer number in a right-open range. The gen generic method generates a number of the specified type. Sometimes, this type can be inferred, like in the last statement, where a Boolean is expected. If the generated type is a floating-point number, it is between 0 and 1, with 1 excluded.

Logging – the log crate

For any kind of software, in particular for servers, the ability to emit logging messages is essential. The logging architecture has two components:

  • API: Defined by the log crate
  • Implementation: Defined by several possible crates

Here, an example using the popular env_logger crate is shown. If you want to emit logging messages from a library, you should only add the API crate as a dependency, as it is the responsibility of the application to define the logging implementation crate.

In the following example (named use_env_logger), we are showing an application (not a library), and so we need both crates:

#[macro_use]
externcratelog;

fnmain() {
env_logger::init();
error!("Error message");
warn!("Warning message");
info!("Information message");
debug!("Debugging message");
}

In a Unix-like console, after having run cargo build, execute the following command:

          RUST_LOG=debug ./target/debug/use_env_logger
        

It will print something like the following:

          [2020-01-11T15:43:44Z ERROR logging] Error message
          
[2020-01-11T15:43:44Z WARN logging] Warning message
[2020-01-11T15:43:44Z INFO logging] Information message
[2020-01-11T15:43:44Z DEBUG logging] Debugging message

By typing RUST_LOG=debug at the beginning of the command, you defined the temporary environment variable RUST_LOG, with debug as its value. The debug level is the highest, and hence all logging statements are performed. Instead, if you execute the following command, only the first three lines will be printed, as the info level is not detailed enough to print debug messages:

          RUST_LOG=info ./target/debug/
          use_env_logger
        

Similarly, if you execute the following command, only the first two lines will be printed, as the warn level is not detailed enough to print either the debug or the info messages:

          RUST_LOG=warn ./target/debug/
          use_env_logger
        

If you execute one or the other of the following commands, only the first line will be printed, as the default logging level is error:

  • RUST_LOG=error ./target/debug/use_env_logger
  • ./target/debug/use_env_logger

Initializing static variables at runtime – the lazy_static crate

It's well known that Rust does not allow mutable static variables in safe code. Immutable static variables are allowed in safe code, but they must be initialized by constant expressions, possibly by invoking const fn functions. However, the compiler must be able to evaluate the initialization expression of any static variable.

Sometimes, however, there is a need to initialize a static variable at runtime, because the initial value depends on an input, such as a command-line argument or a configuration option. In addition, if the initialization of a variable takes a long time, instead of initializing it at the start of the program, it may be better to initialize it only the first time the variable is used. This technique is called lazy initialization.

There is a small crate, named lazy_static, that contains only one macro, which has the same name as the crate. This can be used to solve the issue mentioned previously. Its use is shown in the following project (named use_lazy_static):

use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
static ref DICTIONARY: HashMap<u32, &'static str> = {
let mut m = HashMap::new();
m.insert(11, "foo");
m.insert(12, "bar");
println!("Initialized");
m
};
}

fn main() {
println!("Started");
println!("DICTIONARY contains {:?}", *DICTIONARY);
println!("DICTIONARY contains {:?}", *DICTIONARY);
}

This will print the following output:

          Started
          
Initialized
DICTIONARY contains {12: "bar", 11: "foo"}
DICTIONARY contains {12: "bar", 11: "foo"}

As you can see, the main function starts first. Then, it tries to access the DICTIONARY static variable, and that access causes the initialization of variables. The initialized value, which is a reference, is then dereferenced and printed.

The last statement, which is identical to the previous one, does not perform the initialization again, as you can see by the fact that the Initialized text is not printed again.

Parsing the command line – the structopt crate

The command-line arguments of any program are easily accessible through the std::env::args() iterator. However, the code that parses these arguments is actually rather cumbersome. To get more maintainable code, the structopt crate can be used, as shown in the following project (named use_structopt):

usestd::path::PathBuf;
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
struct Opt {
/// Activate verbose mode
#[structopt(short = "v", long = "verbose")]
verbose: bool,

/// File to generate
#[structopt(short = "r", long = "result", parse(from_os_str))]
result_file: PathBuf,

/// Files to process
#[structopt(name = "FILE", parse(from_os_str))]
files: Vec<PathBuf>,
}

fn main() {
println!("{:#?}", Opt::from_args());
}

If you execute the cargo run input1.txt input2.txt -v --result res.xyzcommand, you should get the following output:

Opt {
verbose: true,
result_file: "res.txt",
files: [
"input1.tx",
"input2.txt"
]
}

As you can see, the filenames input1.txt and input2.txt have been loaded into the files field of the structure. The --result res.xyz argument caused the result_file field to be filled, and the -v argument caused the verbose field to be set to true, instead of the default false.

Summary

In this chapter, we introduced the new Rust 2018 edition. We learned about the kind of projects that are going to be described in this book. We then took a quick look at four useful crates which you can apply in your Rust code.

In the next chapter, we will learn how to store or retrieve data to and from a file, a database, or another application.

Questions

  1. Is there an official printed book to learn the Rust language?
  2. How long was the longest primitive Rust integer in 2015, and how long was it at the end of 2018?
  3. Which are the four domain working groups at the end of 2018?
  4. What is the purpose of the Clippy utility?
  5. What is the purpose of the rustfix utility?
  6. Write a program that generates 10 pseudo-random f32 numbers between 100 and 400.
  7. Write a program that generates 10 pseudo-random i32 numbers between 100 and 400 (without truncating or rounding the numbers generated by the previous exercise).
  8. Write a program that creates a static vector containing all squared integers between 1 and 200.
  9. Write a program that emits a warning message and an info message, and then run it so that only the warning message appears.
  10. Try to parse a command-line argument that contains a value from 1 to 20, emitting an error message if the value is out of range. The short option should be -l, and the long option should be --level.
..................Content has been hidden....................

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