- Creative Projects for Rust Programmers
- Carlo Milanesi
- 1068字
- 2021-06-18 19:01:56
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.