Book Image

Creative Projects for Rust Programmers

By : Carlo Milanesi
Book Image

Creative Projects for Rust Programmers

By: Carlo Milanesi

Overview of this book

Rust is a community-built language that solves pain points present in many other languages, thus improving performance and safety. In this book, you will explore the latest features of Rust by building robust applications across different domains and platforms. The book gets you up and running with high-quality open source libraries and frameworks available in the Rust ecosystem that can help you to develop efficient applications with Rust. You'll learn how to build projects in domains such as data access, RESTful web services, web applications, 2D games for web and desktop, interpreters and compilers, emulators, and Linux Kernel modules. For each of these application types, you'll use frameworks such as Actix, Tera, Yew, Quicksilver, ggez, and nom. This book will not only help you to build on your knowledge of Rust but also help you to choose an appropriate framework for building your project. By the end of this Rust book, you will have learned how to build fast and safe applications with Rust and have the real-world experience you need to advance in your career.
Table of Contents (14 chapters)

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]
extern crate log;

fn main() {
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):

use std::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.xyz command, 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.