16 Const, “unsafe” Rust, and external crates

This chapter covers

It’s now time to learn about Rust’s third generic type (const generics) and all the other things to do with const and static in Rust. Const generics let you be generic over const values, which is most useful when working with arrays. Const functions are similar to regular functions, but they can be called at compile time before your program starts. We will also start to learn about the unsafe side of Rust, starting with static mut, a static that is unsafe to use. We’ll also learn about why unsafe Rust even exists and why you might never even need to touch it. Then we will start moving into external crates, which, thanks to Cargo, are extremely easy to use.

16.1 Const generics

Up to now, we have learned two types of generic parameters in Rust:

With const generics, we will now encounter the third and final generic parameter used in Rust. Const generics let items be generic over const values. Const generics were implemented fairly recently in Rust, in 2021. A lot of people wanted to see const generics because of difficulties with arrays.

NOTE The three types of generics can be seen in the Rust Reference here: https://doc.rust-lang.org/reference/items/generics.html. They are officially known as LifetimeParam, TypeParam, and ConstParam.

Let’s look at what the pain point was when working with arrays before const generics. We learned that one array can only be the same type as another array if it holds both the same type and the same number of items. So, an [i32; 3] is not the same type as an [i32; 4] even though the second one only has one more item. This strictness in arrays made them quite difficult to work with before const generics were introduced.

To get a feel for this strictness, let’s imagine a struct with two arrays. These two arrays contain some u8s and are probably byte buffers used to hold some data. Without const generics, you have to say exactly how many items it will have:

struct Buffers {
    array_one: [u8; 640],
    array_two: [u8; 640]
}

This works, but what if we want a larger buffer, such as 1,280 bytes instead of 640? That would require a new struct. Let’s put one in:

struct Buffers {
    array_one: [u8; 640],
    array_two: [u8; 640]
}
 
struct BigBuffers {
    array_one: [u8; 1280],
    array_two: [u8; 1280]
}

Any other array size will require a new struct, too. Now, let’s think about implementing a trait for our Buffers or BigBuffers struct. What if we want to implement a trait like Display? We would have to implement the trait for each one. What if we want a lot of different array sizes? We’d need a different struct for each, and each struct would need to implement the traits.

NOTE Rust users had to use macros a lot for these types of structs before const generics were implemented. One Reddit user noted back in 2019: “By far the single biggest pain point is const_generics. It can’t be implemented and stabilized fast enough. I wrote an elaborate system of macros to solve the issue for our particular system” (http://mng.bz/eE8V).

Let’s look at how const generics make this easy. We’ll turn the Buffers struct into this:

struct Buffers<T, const N: usize> {
    array_one: [T; N],
    array_two: [T; N]
}

Now, we only need a single struct to do what we were attempting to do before. Our Buffers struct is generic in two ways. First, it is generic over a type T, and this will be a u8 or an i32 or something like that. And the second generic is a const generic, which we are calling N, and it’s a usize. The const keyword here shows us that it is a const generic. Only usize will work here because Rust uses usize to index arrays. So the type here is fixed, but the number is not: it’s N and can be any number.

Let’s give it a try with some really small arrays so we can print them out here:

#[derive(Debug)]                     
struct Buffers<T, const N: usize> {
    array_one: [T; N],
    array_two: [T; N],
}
 
fn main() {
    let buffer_1 = Buffers {
        array_one: [0u8; 3],
        array_two: [0; 3],
    };
 
    let buffer_2 = Buffers {
        array_one: [0i32; 4],
        array_two: [10; 4],
    };
 
    println!("{buffer_1:#?}, {buffer_2:#?}");
}

Now Debug works for any size array, just like for any other struct!

The code gives us the following output:

Buffers {
    array_one: [
        0,
        0,
        0,
    ],
    array_two: [
        0,
        0,
        0,
    ],
}, Buffers {
    array_one: [
        0,
        0,
        0,
        0,
    ],
    array_two: [
        10,
        10,
        10,
        10,
    ],
}

Const generics are used for more than just arrays, but working with arrays is the main pain point that it solves.

16.2 Const functions

On top of fn, Rust also has a const fn. Rust’s documentation defines a const fn as a function that is “permitted to call from a const context” and adds that in this case “the function is interpreted by the compiler at compile time” (http://mng.bz/g78v). Note the word permitted in the wording: a const fn doesn’t have to be called during compile time, but it always can be. So a const fn can be called anywhere, not just in const contexts. As the reference states, “you can freely do anything with a const function that you can do with a regular function.”

Here’s a quick example:

const NUMBER: u8 = give_eight();
 
const fn give_eight() -> u8 {
    8
}
 
fn main() {
    let mut my_vec = Vec::new();
    my_vec.push(give_eight());
}

This give_eight() function is used to make a const, which is used by NUMBER at compile time to get its value. But then down in main(), the same function is being used to push a number to a Vec, which is an allocation (allocations aren’t allowed at compile time). The function is being used both at compile time and after compile time.

Now, if we change const fn give_eight() to a regular fn give_eight(), it won’t work. Rust complains that our function isn’t const, so it can’t guarantee that it can be called:

error[E0015]: cannot call non-const fn `give_eight` in constants
 --> src/main.rs:1:20
  |
1 | const NUMBER: u8 = give_eight();
  |                    ^^^^^^^^^^^^
  |
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

That’s why not all functions are const: not all things are allowed in a const context (like allocations).

If you want to give a const fn a try, add const to your function and see what the compiler says. You might be able to find a way to make it work.

This is a bit vague, but that’s because what you can do in a const fn in Rust can be a bit vague and is always improving. Const functions were quite limited in the beginning, but the Rust team continues to work on them to allow more and more functionality inside. For example, Rust 1.61 in 2022 added the following:

Several incremental features have been stabilized in this release to enable more functionality in const functions:

Note that the trait features do not yet support calling methods from those traits in a const fn. (http://mng.bz/amAm)

By the time you start learning Rust, there might be more and more things allowed in const fn than when this book was published.

On top of that, each Rust version will usually have a list of functions that are now const. Taking a look at version 1.61 again (http://mng.bz/wjM2), you can see that these functions are const. So they wouldn’t have worked in a const context before, but do now:

The following previously stable functions are now const:
 
<*const T>::offset and <*mut T>::offset
<*const T>::wrapping_offset and <*mut T>::wrapping_offset
<*const T>::add and <*mut T>::add
<*const T>::sub and <*mut T>::sub
<*const T>::wrapping_add and <*mut T>::wrapping_add
<*const T>::wrapping_sub and <*mut T>::wrapping_sub

Okay, those are some pretty obscure functions. Let’s look at some key functions that were made const fairly recently (as of Rust 1.63) because they are pretty useful!

16.3 Mutable statics

Mutable global variables are used a lot in other languages, but in Rust, they are a lot harder. You can imagine why: first, Rust has strict rules on borrowing and mutating data. Second, global variables (consts and statics) are initialized in a const context, which means only a const fn can be used. There are external crates that can help work around this, but in Rust 1.63, a nice change happened: Mutex::new() and RwLock::new() became const functions! With that, you can stick anything inside them that can be made in a const context. That even includes some types we know on the heap because their new() functions don’t allocate. For example, String::new(), Vec::new()became const fns in Rust 1.39, so those are just fine.

Let’s give this a try with a super-simple global logger that is just a Vec<Log>, in which a Log is just a struct with two fields. This code wasn’t possible in Rust before August 2022, so it’s a very nice change to have:

use std::sync::Mutex;
 
#[derive(Debug)]
struct Log {
    date: &'static str,                                            
    message: String,
}
 
static GLOBAL_LOGGER: Mutex<Vec<Log>> = Mutex::new(Vec::new());    
 
fn add_message(date: &'static str) {
    GLOBAL_LOGGER.lock().unwrap().push(Log {                       
        date,
        message: "Everything's fine".to_string(),
    });
}
 
fn main() {
    add_message("2022-12-12");
    add_message("2023-05-05");
    println!("{GLOBAL_LOGGER:#?}");
}

Timestamps are usually i64, but we’ll just use a &str here.

Nothing is inside, so no allocations; thus, it’s fine as a static. And it’s a Mutex, so we can change what’s inside it. Pretty convenient!

GLOBAL_LOGGER is global, so we don’t have to pass it in as a function argument.

This prints

Mutex {
    data: [
        Log {
            date: "2022-12-12",
            message: "Everything's fine",
        },
        Log {
            date: "2023-05-05",
            message: "Everything's fine",
        },
    ],
    poisoned: false,
    ..
}

As you can see, there is nothing new for us to learn here: we are just using a regular Mutex with a regular Vec. As long as they are empty to start with, they can be used as a static and then modified at run time.

16.4 Unsafe Rust

Statics in Rust have another interesting property that brings us to a new subject of discussion: they can actually be mutable. Making a static mutable is as easy as making anything else mutable: just declare a static mut instead of a static. This is another property of statics that makes them much different from a const. And once you have declared a static as mutable, it can be changed by anything at any time throughout the program.

Hopefully, this is already setting off warning bells inside your head! It seems a little too convenient, doesn’t it?

Indeed, mutable statics haven’t been mentioned in the book yet because a static mut can only be used with the unsafe keyword, and Rust has many safer ways to modify static variables compared to the early days of the language. On that note, what is unsafe Rust, and why is the unsafe keyword needed when using a static mut?

16.4.1 Overview of unsafe Rust

So what’s unsafe Rust? Isn’t Rust supposed to be safe?

It is, but Rust is also a systems programming language. That means that you can use it to build an operating system, you can use it for robotics, or anything like that. As an example, hardware often requires sending a signal to a certain memory address to start up or accomplish some other task. The Rust compiler has no idea what is at these memory addresses, so you need to use the unsafe keyword for that.

TIP The Writing an OS in Rust blog has many good examples of the unsafe keyword used for such cases: https://os.phil-opp.com/testing/.

You can also use Rust to work with other languages like C and Javascript. Here again, the Rust compiler has no idea whether their functions are safe or not, as they are entirely different languages. So you use unsafe here, too. For example, in the bindings between Rust and libc (the standard library for the C language), every function (https://docs.rs/libc/latest/libc/#functions) is an unsafe function. A lot of work has been done to make sure that they are as safe as possible, but Rust still can’t make any guarantees because it’s a different language.

There is a lot of discussion about the word “unsafe” because the keyword itself can be a bit shocking, and the keyword unsafe does not necessarily mean that there is anything wrong with a piece of code. After all, anyone can see that this code (100% safe code just wrapped in an unsafe block) is perfectly safe:

fn main() {
    let my_name = unsafe { "My name" };
    println!("{my_name}");
}

But the unsafe keyword was chosen to be shocking on purpose to ensure that people know that the developer now bears more responsibility because the compiler allows some code inside an unsafe block to compile when it would not compile otherwise. In essence, an unsafe block is more like a trust_me_i_know_what_im_doing block.

Outside of the previously mentioned contexts, unsafe is extremely rare. If you are not working with low-level system resources or directly connecting to functions in other languages, you might never use unsafe. Many Rust programmers have never even had to use a single unsafe block of code.

Having said that, let’s take a look at some unsafe for fun. You’ll see this word in unsafe blocks and unsafe fns. A function with unsafe code will need to be called an unsafe fn, and to access it, you’ll need an unsafe block. So this won’t quite work:

unsafe fn uh_oh() {}
 
fn main() {
    uh_oh();
}

The compiler says:

error[E0133]: call to unsafe function is unsafe and requires unsafe
function or block
 --> src/main.rs:6:5
  |
6 |     uh_oh();
  |     ^^^^^^^ call to unsafe function
  |

That’s easy to fix; just add an unsafe block:

unsafe fn uh_oh() {}
 
fn main() {
    unsafe {
        uh_oh();
    }
}

Done!

NOTE If you find yourself enjoying this section on unsafe Rust, you might also be pleased to know that there is a whole book on unsafe code! It’s called The Rustonomicon and can be read here: https://doc.rust-lang.org/nomicon/index.html.

16.4.2 Using static mut in unsafe Rust

Now let’s look at what a static mut is. As the name suggests, it is simply a static that can be directly changed—no need for a Mutex or any other sort of wrapper to do so. Let’s give one a try. This code almost compiles:

static mut NUMBER: u32 = 0;
 
fn main() {
    NUMBER += 1;
    println!("{NUMBER}");
}

However, the compiler won’t let us modify or even print NUMBER unless we put it in a block marked unsafe. It also tells us why mutable statics are unsafe:

error[E0133]: use of mutable static is unsafe and requires unsafe function
or block
 --> src/main.rs:4:5
  |
4 |     NUMBER += 1;
  |     ^^^^^^^^^^^ use of mutable static
  |
  = note: mutable statics can be mutated by multiple threads: aliasing
violations or data races will cause undefined behavior

So the reason is “aliasing violations or data races will cause undefined behavior.” You’ll see the term “undefined behavior” a lot, sometimes abbreviated as UB, when people discuss unsafe Rust. Avoiding undefined behavior is the reason why we use types like Arc<Mutex> to ensure that access happens the way we expect it to. Let’s see whether we can make some undefined behavior with this static mut. We will spawn some threads, modify NUMBER, and see what happens.

In this example, we will spawn 10 threads, and each one will have a for loop that loops 10 times, increasing NUMBER by 1 each time. With each of the 10 threads incrementing NUMBER 10 times, we are expecting to see a final result of 100:

static mut NUMBER: u32 = 0;
 
fn main() {
    let mut join_handle_vec = vec![];
    for _ in 0..10 {
        join_handle_vec.push(std::thread::spawn(|| {
            for _ in 0..10 {
                unsafe {
                    NUMBER += 1;
                }
            }
        }));
    }
    for handle in join_handle_vec {
        handle.join().unwrap();
    }
    unsafe {
        println!("{NUMBER}");
    }
}

And the result is 100! No problem yet. Let’s bump the numbers up. Now, we will use 1,000 threads, and each thread will loop 1,000 times. The code will be the same as the previous code; we are just changing each for _ in 0..10 to for _ in 0..1000. Because 1,000 times 1,000 is 1,000,000, we now expect to see 1,000,000 as the final number.

But the output is 959,696. Or 853,775. Or 825,266. Or anything else. Now we can see why static mut is unsafe. Each thread is adding 1 to NUMBER for each loop, but sometimes a thread is accessing NUMBER at the same time as another one. If you add println!("{NUMBER}"}; just after NUMBER += 1;, you will see this sort of output in the middle of all the incrementing:

225071
225072    
225073    
225073    

Adds 1. Looks good.

Adds 1. Looks good.

Uh oh . . .

In this example, NUMBER had the value 225,072, two threads accessed it, each added 1, and gave NUMBER the new value 225,073. The threads each did what they were supposed to do, but nothing was keeping them from accessing NUMBER at the same time.

And with this, we can now understand another big difference between const and static: a const is an unchangeable value that is evaluated at compile time, while a static is a static location in memory. There is technically no rule that a static cannot be mut.

16.4.3 Rust’s most famous unsafe method

Now, let’s look at Rust’s most famous unsafe function, transmute(). The documentation for the function explains it as follows: “Reinterprets the bits of a value of one type as another type. Both types must have the same size” (https://doc.rust-lang.org/std/mem/fn.transmute.html).

So with transmute(), you essentially take the bits of one type and tell the compiler: “take these bits and use them as a different type.” Here’s the function signature:

fn transmute<T, U>(e: T) -> U

So you tell it which two types (T, U) it will work on and give it a T, and it returns it as a U.

Let’s try something simple. We’ll make an i32 and tell Rust that it’s now a u32. Both i32 and u32 have a length of 4 bytes, so the code will compile:

use std::mem::transmute;
 
fn main() {
    let x = 19;
    let y = unsafe { transmute::<i32, u32>(x) };
    println!("{y}");
}

That prints 19, simple enough. What if we make x a -19 instead? A u32 can’t be negative, so it can’t possibly end up as the same -19 value. Let’s try that again and see what happens:

use std::mem::transmute;
 
fn main() {
    let x = -19;
    let y: u32 = unsafe { transmute::<i32, u32>(x) };
    println!("{y}");
}

Now it prints 4294967277. Quite different! Remember how to format println! to display bytes that we learned near the beginning of the book? You use {:b} to do it. If transmute() is just reinterpreting the same bytes, then -19 and 4294967277u32 should look the same as bytes. Let’s give it a try:

fn main() {
    println!("{:b}\n{:b}", -19, 4294967277u32);
}

Indeed they do! We get the following output, which shows, indeed, that transmute() is taking the same bytes and treating them differently:

11111111111111111111111111101101
11111111111111111111111111101101

Okay, let’s see whether we can be even more unsafe by transmuting something more complex. Let’s make a User struct with a bit of basic info and see what its size is:

struct User {
    name: String,
    number: u32,
}
 
fn main() {
    println!("{}", std::mem::size_of::<User>());
}

It’s 32 bytes. So what happens if we give Rust an array of eight i32s and tell it to make a User? Both of these are 32 bytes in length, so the program will compile, and transmute() will simply tell Rust to treat these bytes as a User. Let’s see what happens:

use std::mem::transmute;
 
struct User {
    name: String,
    number: u32,
}
 
fn main() {
    let some_i32s = [1, 2, 3, 4, 5, 6, 7, 8];
    let user = unsafe { transmute::<[i32; 8], User>(some_i32s) };
}

Whoops! We got a segmentation fault:

timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Segmentation fault
timeout --signal=KILL ${timeout} "$@"

The transmute() documentation (https://doc.rust-lang.org/std/mem/fn.transmute.html) puts it this way:

Both the argument and the result must be valid at their given type. The compiler will generate code assuming that you, the programmer, ensure that there will never be undefined behavior. It is therefore your responsibility to guarantee that every value passed to transmute is valid at both types Src and Dst. Failing to uphold this condition may lead to unexpected and unstable compilation results. This makes transmute incredibly unsafe. transmute should be the absolute last resort.

Any programmer choosing to use transmute() has been warned in advance!

16.4.4 Methods ending in _unchecked

The most common and “safest” form of unsafe is probably seen in the _unchecked methods that a lot of types have. For example, Option and Result have unsafe .unwrap_unchecked() methods that assume you have a Some or an Ok and will unwrap without checking. But if you don’t have a Some or an Ok, then undefined behavior will happen. People will sometimes try these methods to see whether there is any performance improvement in their code. In that case, you will usually see a note like this to explain why unsafe is being used:

fn main() {
    let my_option = Some(10);
    // SAFETY: my_option is declared as Some(10). It will never be None
    let unwrapped = unsafe {
        my_option.unwrap_unchecked()
    };
    println!("{unwrapped}");
}

This will print 10, and no problems will happen. But once again, using an unsafe function means that all the responsibility is on you. In the previous example, if you change the first line to let my_option: Option<i32> = None; and run it on the Playground, it will dump the core:

     Running `target/debug/playground`
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Illegal instruction
timeout --signal=KILL ${timeout} "$@"

That’s pretty bad.

And there is no guarantee that the _unchecked methods will be faster either. Sometimes, the compiler can use information from the checks in the non-unsafe methods to speed up your code, resulting in _unchecked being slower than the regular safe methods. It can be fun to experiment, but when in doubt, don’t use unsafe!

To sum up:

In the early days of Rust, users of the language emphasized how easy it was to link it to C and C++ libraries. (Here is one example from 2015: http://mng.bz/qjnJ). But as time has gone by, more and more libraries have been written in pure Rust, and it has become quite rare to see unsafe in a Rust external crate. And a lot of other interesting developments are going on to help use unsafe even less frequently. There is even a working group to make the transmute() function safe (http://mng.bz/7vwe)!

On that note, it’s time to turn our attention to external crates.

16.5 Introducing external crates

An external crate simply means a crate that isn’t the one that you are working on and is usually someone else’s crate. We learned in chapter 14 about modules and structuring your code for others to use, which is the first step to creating an external crate. When people write crates they think might be useful for others, they publish them on https://crates.io/, and those become usable by anyone else. As of early 2024, over 130,000 crates have been published!

For this section, you almost need to install Rust, but we can still use just the Playground. That’s because the Playground has all the most-used external crates already installed. Using external crates is important in Rust for two reasons: it is incredibly easy to import other crates, and the Rust standard library is quite small.

That means that it is normal in Rust to bring in an external crate for a lot of basic functions, and that’s why the Playground includes so many. The idea is that if it is easy to use external crates, you can choose the best one. Often, one person will make a crate that provides some functionality, and then someone else will make a similar and possibly better one.

In this book, we will only look at the most popular crates, the crates that everyone who uses Rust knows. To begin learning external crates, we will start with a pretty simple one: rand.

16.5.1 Crates and Cargo.toml

Have you noticed that we haven’t used any random number functions yet in this book? That’s because random numbers aren’t in the standard library. But there are a lot of crates that are “almost standard library” because everybody uses and trusts them. These crates are also nicknamed Rust’s “blessed crates,” and there is even a website (https://blessed.rs/crates) that lists them (called blessed.rs!). The crate rand is one of these “blessed crates.”

In any case, it’s very easy to bring in a crate. If you have a Cargo (Rust) project on your computer, you should notice a file called Cargo.toml that has this information. The Cargo.toml file looks like this when you start:

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-
lang.org/cargo/reference/manifest.html
 
[dependencies]

Now, if you want to add the rand crate, go to https://crates.io. Search for rand and click on it. Now you are at https://crates.io/crates/rand. Click in the box under Or Add the Following Line to Your Cargo.toml, which is located on the right-hand side of the page, to copy it. Then just add it under [dependencies] like this:

[package]
name = "rust_book"
version = "0.1.0"
authors = ["David MacLeod"]
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
[dependencies]
rand = "0.8.5"

Cargo will do the rest for you. Or you can use the command cargo add rand on the command line, and it will do the same thing. Then, when you type cargo run to run your program, it will automatically bring in the code to use rand, and you will be able to use this crate in the same way that we’ve been using code from the standard library. The only difference is that the first part of the path will be rand instead of std.

To get to the documents for rand, you can click on the docs button on its page on crates.io, which will take you to the documentation (https://docs.rs/rand/latest/rand/). Fortunately the documentation looks the same as that in the standard library! Thanks to a standardized layout for documentation, you won’t have any trouble looking around the rand crate to see what it holds. Now let’s give rand a try.

16.5.2 Using the rand crate

We are still using the Playground by default, which fortunately already has the top 100 crates installed. On the Playground, you can imagine that it has a long list like this with 100 crates:

[dependencies]
rand = "0.8.5"
some_other_crate = "0.1.0"
another_nice_crate = "1.7"

And so on. So that means we don’t need to look at Cargo.toml again until a bit later in the book. So, to use rand, you can just do this:

use rand::random;     
 
fn main() {
    for _ in 0..5 {
        let random_u16 = random::<u16>();
        print!("{random_u16} ");
    }
}

This means the whole crate rand. On your computer, you can’t simply write this; you need to write in the Cargo.toml file first.

This code will print a different u16 number every time, like 42266 52873 56528 46927 6867.

The main functions in rand are random() and thread_rng() (rng means “random number generator”). If you look at random(), it says: “This is simply a shortcut for thread_rng().gen().” So, it’s actually thread_rng() that does almost everything.

Here is a simple example of numbers from 1 to 10. To get those numbers, we use .gen_range() between 1 and 11.

use rand::{thread_rng, Rng}; 
 
fn main() {
    let mut number_maker = thread_rng();     
    for _ in 0..5 {
        print!("{} ", number_maker.gen_range(1..11));
    }
}

Or we can just use rand::*; if we are lazy.

This will print something like 7 2 4 8 6.

16.5.3 Rolling some dice with rand

With random numbers, we can do fun things like make characters for a game. In this game, our characters have six stats, and you use a d6 for them. A d6 is a die (a cube) that gives 1, 2, 3, 4, 5, or 6 when you throw it. Each character rolls a d6 three times, so each stat is between 3 and 18.

But sometimes it can be unfair if your character has a really low stat, like a 3 or 4. If your strength is 3, you can’t carry anything, for example. And a character that rolls 3 for intelligence won’t even be smart enough to know how to speak. Because of this, there is one more dice rolling method that rolls a d6 four times and throws away the lowest number. So, if you roll 3, 3, 1, and 6, you throw out the 1 and keep 3, 3, and 6, giving a value of 12 (instead of 7). This method keeps characters from having stats that are too low while still keeping 18 as the maximum.

We will make a simple character creator that lets you choose between rolling three times and rolling four times. We create a Character struct for the stats and have a function to roll the dice that takes an enum to choose between rolling three or four times:

use rand::{thread_rng, Rng};
 
#[derive(Debug)]
struct Character {
    strength: u8,
    dexterity: u8,
    constitution: u8,
    intelligence: u8,
    wisdom: u8,
    charisma: u8,
}
 
#[derive(Copy, Clone)]                                    
enum Dice {
    Three,
    Four,
}
 
fn roll_dice(dice_choice: Dice) -> u8 {
    let mut generator = thread_rng();
    let mut total = 0;
    match dice_choice {
        Dice::Three => {
            for _ in 0..3 {
                total += generator.gen_range(1..=6);
            }
        }
        Dice::Four => {
            let mut results = vec![];                     
            (0..4).for_each(|_| results.push(generator.gen_range(1..=6)));
            results.sort();
            results.remove(0);
            total += results.into_iter().sum::<u8>();
        }
    }
    total
}
 
impl Character {
    fn new(dice_choice: Dice) -> Self {
        let mut stats = (0..6).map(|_| roll_dice(dice_choice));
        Self {
            strength: stats.next().unwrap(),              
            dexterity: stats.next().unwrap(),
            constitution: stats.next().unwrap(),
            intelligence: stats.next().unwrap(),
            wisdom: stats.next().unwrap(),
            charisma: stats.next().unwrap(),
        }
    }
}
 
fn main() {
    let weak_billy = Character::new(Dice::Three);
    let strong_billy = Character::new(Dice::Four);
    println!("{weak_billy:#?}");
    println!("{strong_billy:#?}");
}

Dice doesn’t hold any data so we might as well make it both Copy and Clone.

We can’t just add the numbers to the total when rolling four dice, so we will first put them all in a Vec. Then we’ll use .sort() and remove the 0th item (the smallest).

We’re confident that our stats iterator is six items in length so we’ll just unwrap for each.

It will print something like this:

Character {
    strength: 11,
    dexterity: 9,
    constitution: 9,
    intelligence: 8,
    wisdom: 7,
    charisma: 13,
}
Character {
    strength: 15,
    dexterity: 13,
    constitution: 5,
    intelligence: 13,
    wisdom: 14,
    charisma: 15,
}

As you can see, the character with four dice rolls is usually a bit better at most things.

That was easy! We’ve learned that with a single line in Cargo.toml you can use external crates in the same way that we’ve been using the standard library. We also learned about const generics, which you won’t see as much as regular generics and lifetimes but is good to understand. And we now have an idea of why Rust as a language needs unsafe in certain situations and why you almost never need to use it unless you are working with rare cases like embedded software or calling into other languages.

We have only scraped the surface of the fantastic external crates that Rust has to offer, so in the next chapter, we will look at some more of the “blessed” ones. Even though they are technically external crates, you can almost think of them as extensions of the standard library. You will definitely want to be familiar with their names and what they do.

Summary