Book Image

Rust Programming Cookbook

By : Claus Matzinger
Book Image

Rust Programming Cookbook

By: Claus Matzinger

Overview of 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.
Table of Contents (12 chapters)

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.