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)

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.