21 Continuing the tour

This chapter covers

This is the second of two chapters touring the standard library, with a lot of new (but not difficult) types that you’ll find useful to know. Near the end, we will look at some macros we haven’t encountered before, which will lead into the next chapter, where you will learn how to write your own!

21.1 std::mem

As the name implies, the std::mem module has types and functions for dealing with memory. The functions inside this module are particularly interesting (and convenient). We have seen some of them already, such as size_of(), size_of_val(), and drop():

use std::mem;
 
fn main() {
    println!("Size of an i32: {}", mem::size_of::<i32>());
    let my_array = [8; 50];
    println!("Size of this array: {}", mem::size_of_val(&my_array));
    let some_string = String::from("Droppable because it's not Copy");
    drop(some_string);
    // some_string.clear();     
}

If we uncommented this, it wouldn’t compile.

This prints

Size of an i32: 4
Size of this array: 200

Note that in the previous code we didn’t need to write mem::drop(), just drop(), because this function is part of the prelude. We will look at the prelude shortly.

Technically, you can call drop() on a Copy type, but it will have no effect. As the function documentation states:

This effectively does nothing for types which implement Copy, e.g., integers. Such values are copied and then moved into the function, so the value persists after this function call. (https://doc.rust-lang.org/std/mem/fn.drop.html)

Let’s look at some other functions in std::mem.

The swap() function lets you switch the values between two variables. You use a mutable reference for each to do it. This is particularly helpful when you have two things you want to switch and Rust doesn’t let you because of borrowing rules or because the parameter isn’t an Option that you could use take() to replace with None. (More on the take() function in a moment.)

Let’s use this function to do some owner swapping of the One Ring from Lord of the Rings:

use std::mem;
 
#[derive(Debug)]
struct Ring {
    owner: String,
    former_owners: Vec<String>,
}
 
impl Ring {                                                          
    fn switch_owner_to(&mut self, name: &str) {
        if let Some(position) = self.former_owners.iter().position(|n| n ==
        name) {
            mem::swap(&mut self.owner, &mut self.former
            _owners[position])                                     
        } else {
            println!("Nobody named {name} found in former_owners, sorry!");
        }
    }
}
 
fn main() {
    let mut one_ring = Ring {                                        
        owner: "Frodo".into(),
        former_owners: vec!["Gollum".into(), "Sauron".into()],
    };
 
    println!("Original state: {one_ring:?}");
    one_ring.switch_owner_to("Gollum");                              
    println!("{one_ring:?}");
    one_ring.switch_owner_to("Sauron");
    println!("{one_ring:?}");
    one_ring.switch_owner_to("Billy");
    println!("{one_ring:?}");
}

This method will try to find a character inside former_owners that matches the search key and, if so, switch owners. We could return a Result or Option here, but to keep it simple, we’ll print an error message if no matching character is found.

Directly accessing a Vec through an index can be risky, so we’ll use the position method to ensure that we found a String that matches the name we are searching for.

For most of the book, the owner of the ring is Frodo, while both Gollum and Sauron are looking for it.

Now let’s do some owner switching.

This will print

Original state: Ring { owner: "Frodo", former_owners: ["Gollum", "Sauron"] }
Ring { owner: "Gollum", former_owners: ["Frodo", "Sauron"] }
Ring { owner: "Sauron", former_owners: ["Frodo", "Gollum"] }
Nobody named Billy found in former_owners, sorry!
Ring { owner: "Sauron", former_owners: ["Frodo", "Gollum"] }

The next function inside std::mem is called replace(). It is similar to .swap() and actually uses swap() inside. The function is extremely simple:

pub fn replace<T>(dest: &mut T, mut src: T) -> T {
    swap(dest, &mut src);
    src
}

So, replace()does a swap and then returns the other item—that’s all there is to it. In other words, .replace() replaces the value with what you put in and returns the old value, which makes it useful with a let binding to create a variable. Here’s a quick example:

use std::mem;
 
struct City {
    name: String,
}
 
impl City {
    fn change_name(&mut self, name: &str) {
        let former = mem::replace(&mut self.name, name.to_string());
        println!("{former} is now called {new}.", new = self.name);
    }
}
 
fn main() {
    let mut capital_city = City {
        name: "Constantinople".to_string(),
    };
    capital_city.change_name("Istanbul");
}

This code prints Constantinople is now called Istanbul.

Now, let’s get to the function take() that we previously mentioned. As the name implies, this function outright takes the value from something and returns it. But take() doesn’t drop the existing variable; instead, it leaves its default value in its place. And that’s why this function requires the type we are take()-ing from to implement Default:

pub fn take<T>(dest: &mut T) -> T
where
    T: Default,

So you can do something like this:

use std::mem;
 
fn main() {
    let mut number_vec = vec![8, 7, 0, 2, 49, 9999];
    let mut new_vec = vec![];
 
    number_vec.iter_mut().for_each(|number| {
        let taker = mem::take(number);
        new_vec.push(taker);
    });
    println!("{:?}\n{:?}", number_vec, new_vec);
}

As you can see, it replaced all the numbers with 0: no index was deleted:

[0, 0, 0, 0, 0, 0]
[8, 7, 0, 2, 49, 9999]

Of course, for your own type you can implement Default to whatever you want. The following code shows a bank in a country called Klezkavania that gets robbed all the time. Every time it gets robbed, it replaces the money at the front with 50 credits (the default):

use std::mem;
 
#[derive(Debug)]
struct Bank {
    money_inside: u32,
    money_at_desk: DeskMoney,
}
 
#[derive(Debug)]
struct DeskMoney(u32);
 
impl Default for DeskMoney {
    fn default() -> Self {
        Self(50)                            
    }
}
 
fn main() {
    let mut bank_of_klezkavania = Bank {    
        money_inside: 5000,                 
        money_at_desk: DeskMoney(500),      
    };
 
    let money_stolen = mem::take(&mut bank_of_klezkavania.money_at_desk);
    println!("Stole {} Klezkavanian credits", money_stolen.0);
    println!("{bank_of_klezkavania:?}");
}

default is always 50, not 0.

Sets up our bank

This will print

Stole 500 Klezkavanian credits
Bank { money_inside: 5000, money_at_desk: DeskMoney(50) }

You can see that there is always $50 at the desk.

In practice, the take() function is often used as a convenience method to quickly turn a Some into a None without having to do any pattern matching. The following example shows just how short your code can be when using this function:

use std::time::Duration;
 
struct UserState {
    username: String,
    connection: Option<Connection>,           
}
 
struct Connection {
    url: String,
    timeout: Duration,
}
 
impl UserState {
    fn is_connected(&self) -> bool {
        self.connection.is_some()
    }
    fn connect(&mut self, url: &str) {        
        self.connection = Some(Connection {
            url: url.to_string(),
            timeout: Duration::from_secs(3600),
        });
    }
    fn disconnect(&mut self) {
        self.connection.take();               
    }
}
 
fn main() {
    let mut user_state = UserState {
        username: "Mr. User".to_string(),
        connection: None,
    };
    user_state.connect("someurl.com");
    println!("Connected? {}", user_state.is_connected());
    user_state.disconnect();
    println!("Connected? {}", user_state.is_connected());
}

We could have the Connection struct hold the state of the connection, but another way to do it is to wrap it in an Option. In this case, Some represents a connected state, and None, a nonconnected state.

A real connect method would be more complicated than this, but you get the idea.

To disconnect, just take() the value and do nothing with it, leaving None in its place.

The output is pretty simple:

Connected? true
Connected? false

21.2 Setting panic hooks

We learned back in chapter 5 that a panic in Rust is actually not very panicky. A panic is simply the program seeing that there is a problem that it can’t deal with, so it gives up. For example, there is nothing a program can do when it sees this code:

println!("{}", vec![1, 2][3]);

The programmer here is telling the program to access the fourth item of a Vec that has two items, and that isn’t allowed, so the only option is to give up. The program then prints a message and unwinds the stack, which cleans up the memory for the thread. And if the thread is the main thread, then the program is over. (Maybe it was named panic because the developer is the one who panics when it happens!)

In any case, because a panic is an orderly process, we can modify its behavior a bit. There is a module in the standard library that is also called std::panic that lets us modify what happens when a panic takes place.

First, let’s review the output we see whenever a program panics. We’ll start by just using the panic! macro, which is the easiest way to do it:

fn main() {
    panic!();
}

The output is

thread 'main' panicked at 'explicit panic', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace

The panic! macro can take a message, which will change the output somewhat:

fn main() {
    panic!("Oh man, something went wrong");
}

Now the output contains our message instead of just 'explicit panic':

thread 'main' panicked at 'Oh man, something went wrong', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace

Now let’s take a look at what happens with a panic that happens outside of the panic! macro. First, we will try to parse a number without unwrapping, which will generate a Result with the error info. After that, we’ll unwrap and compare the panic info to what we printed out:

fn main() {
    let try_parse = "my_num".parse::<u32>();
    println!("Error output: {try_parse:?}");
    let my_num = try_parse.unwrap();
}

We can see that when a panic happens, it first tells us which thread panicked, why it panicked, any error info, the location in the code where the panic happened, and a note about how to display a backtrace:

Error output: Err(ParseIntError { kind: InvalidDigit })
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
ParseIntError { kind: InvalidDigit }', src/main.rs:4:28
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace

So, that’s the default behavior. But we can change all of this if we want by using a method called set_hook(). This sets up a global panic hook, which will be called instead of the default panic hook. Inside this method is a closure in which we can do whatever we like when a panic happens. Let’s make a really simple one that prints out a message or two when a panic happens—one in English and another in Korean:

fn main() {
    std::panic::set_hook(Box::new(|_| {
        println!("Oops, that didn't work.");
        println!("앗 뭔가 잘못 됐네요.");        
    }));
    
    panic!();
}

Korean for “Oops, something’s gone wrong.”

The panic hook does exactly what we told it to do, and even the location and error information is gone. This is all we see now when we run the program:

Well, that didn't work.
앗 뭔가 잘못 됐네요.

Where does the default information in a panic message come from? It would be nice if we could display that as well.

You might have noticed that inside set_hook() is a closure with an argument that we ignored by using |_|. If we give the argument a name, we can see that it is a struct called PanicInfo, which implements both Debug and Display. Let’s print it out:

fn main() {
    std::panic::set_hook(Box::new(|info| {
        println!("Well, that didn't work: {info}");
    }));
    panic!();
}

This will print

Well, that didn't work: panicked at 'explicit panic', src\main.rs:6:5

The PanicInfo struct itself is fairly interesting, as it has a parameter called payload that implements Any:

pub struct PanicInfo<'a> {
    payload: &'a (dyn Any + Send),
    message: Option<&'a fmt::Arguments<'a>>,
    location: &'a Location<'a>,
    can_unwind: bool,
}

Back in chapter 13 we learned that the Error trait has methods that let us try to downcast it into a concrete type. The Any trait is another trait that includes methods for downcasting and is automatically implemented on any type unless it contains a non-'static reference. In other words, the payload parameter will hold a trait object behind, which technically could be almost anything.

However, the documentation for the .payload() method for the PanicInfo struct tells us which type the payload will usually be:

Returns the payload associated with the panic.
This will commonly, but not always, be a &'static str or String.

Let’s give this downcasting a try:

fn main() {
    std::panic::set_hook(Box::new(|info| {
        if let Some(payload) = info.payload().downcast_ref::<&str>() {
            println!("{payload}");    
        } else {
            println!("No payload!");
        }        
    }));
    panic!("Oh no");
}

The .downcast_ref::<&str>() method returns a Some, so this will simply print Oh no.

This brings us to an important point: .downcast_ref() is a method that can fail, so we made sure to use if let to ensure that a panic did not occur. Avoiding panics is important in any case, but when setting a panic hook, it is especially important because a panic during a panic will lead to an abort, which means it won’t unwind the stack and just hand everything over to the operating system to clean up.

The output for an abort will depend on your operating system, but it can look pretty ugly. On the Playground, a panic inside a panic (in other words, an abort) looks like this:

thread panicked while processing panic. aborting.
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Aborted
timeout --signal=KILL ${timeout} "$@"

And on Windows it will look something like this:

thread panicked while processing panic. aborting.
error: process didn't exit successfully: `target\release\rmol.exe` (exit
code: 0xc0000409, STATUS_STACK_BUFFER_OVERRUN)

So, be sure to be extra careful when setting a panic hook.

This does bring up an interesting point, though: sometimes people will choose to abort by default instead of panicking because that can reduce the size of the binary by a bit. The smaller size is because there will now be no cleanup code inside the binary. You don’t want to set a panic inside a panic hook to do this, though. Instead, just add this to your Cargo.toml file:

[profile.release]
panic = 'abort'

Then run or build the binary in release mode (cargo run –release or cargo build –release), and it will be a little smaller. But the vast majority of the time, you won’t want to abort when a panic happens.

Let’s finish up this section with a somewhat larger (but imaginary) example of where we might want to use a panic hook. Here, we are running some sort of software that accesses a database. We want to make sure that the database gets shut down properly even if there is a panic, before the stack unwinds and the program stops. We’ll also add some pretend types and functions that demonstrate how our system is working and what sometimes goes wrong:

use rand::Rng;
 
struct Database {
    data: Vec<String>,
}
 
fn get_hour() -> u32 {
    let mut rng = rand::thread_rng();
    rng.gen_range(0..=30)                                   
}
 
fn shut_down_database(hour: u32) -> Result<(), String> {    
    match hour {
        h if (6..18).contains(&h) => {
                                                            
            Ok(())
        }
        h if h > 24 => Err(format!("Internal error: hour {h} shouldn't
        exist")),
        h => Err(format!("Hour {h} is not working hours, can't shut down")),
    }
}
 
fn main() {
    std::panic::set_hook(Box::new(|info| {
        println!("Something went wrong / 문제가 생겼습니다!");
        println!("Panic info: {info}");
        let hour = get_hour();
        match shut_down_database(hour) {
            Ok(()) => println!("Shutting down database at {hour}
            o'clock!"),
            Err(e) => println!("Couldn't shut down database before panic
            finished: {e}"),
        }
    }));
    let mut db = Database { data: vec![] };
    db.data.push("Some data".to_string());
    panic!("Database broke");
}

Uh oh, someone made a mistake, and sometimes the hour of the day is greater than 24. We’ll represent this with a simple function that returns a number up to 30.

This method will only shut down the database during working hours. Outside of working hours, it will leave it running and log a message; it also checks for incorrect hours of the day.

Do some database shutting down stuff.

The output will always include this:

Something went wrong / 문제가 생겼습니다!
Panic info: panicked at 'Database broke', src\main.rs:38:5

The rest of the output will be one of these three lines, depending on the hour of the day returned by get_hour():

Couldn't shut down database before panic finished: Internal error: hour 27
shouldn't exist
Shutting down database at 17 o'clock!
Couldn't shut down database before panic finished: Hour 1 is not working
hours, can't shut down

Finally, what if you want to undo the panic hook? That’s easy, just use the take_hook() method. It looks something like this:

fn main() {
    std::panic::set_hook(Box::new(|_| {
        println!("Something went wrong / 문제가 생겼습니다!");
    }));
 
    let _ = std::panic::take_hook();
    panic!();
}

This will just print out the regular thread 'main' panicked at 'explicit panic', src\main.rs:8:5 panic message. We are using let _ because take_hook() returns the PanicInfo struct that was set in set_hook() in the previous example, and we don’t need it here. But if you do need it, you can give it a variable name and use it however you like.

Before we finish this section, let’s take a quick look at the take_hook() method. Don’t worry about all the details inside, but do you notice a method that we learned in this chapter? There it is—our old friend mem::take()! You can see that it’s being used to grab the old panic hook, after which it returns it to us:

pub fn take_hook() -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
    if thread::panicking() {
        panic!("cannot modify the panic hook from a panicking thread");
    }
    let mut hook = HOOK.write().unwrap_or_else(PoisonError::into_inner);
    let old_hook = mem::take(&mut *hook);
    drop(hook);
    old_hook.into_box()
}

And since mem::take() leaves a default value behind, let’s take a look at the Hook mentioned here to see what it looks like and what its default value is. It’s pretty simple, just an enum that represents either a default panic hook or a custom panic hook.

enum Hook {
    Default,
    Custom(Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>),
}

So, at this point in the book, there’s not much code in the standard library that you can’t understand.

The next part of the standard library is very closely related: backtraces.

21.3 Viewing backtraces

We learned about backtraces in chapter 14 in the section on testing our code. Viewing backtraces when a panic occurs has been a feature of Rust since the language began. However, being able to view a backtrace at run time is fairly new: it was added with Rust 1.65 in November 2022. Before this, the only way to see backtraces at run time was through a crate called backtrace.

But now it can be done without any external code, and viewing a backtrace is pretty simple: just use a function called Backtrace::capture(), which is located in the std::backtrace module. There is one thing to keep in mind, though. Try running this code on the Playground or your computer and see what happens:

use std::backtrace::Backtrace;
 
fn main() {
    println!("{}", Backtrace::capture());
}

This only prints out the following:

disabled backtrace

The documentation explains that this method will look for either a RUST_BACKTRACE or a RUST_LIB_BACKTRACE environment variable. Interestingly, the source code shows us that it only cares if the environment variables are set to 0 or not:

let enabled = match env::var("RUST_LIB_BACKTRACE") {
    Ok(s) => s != "0",
    Err(_) => match env::var("RUST_BACKTRACE") {
        Ok(s) => s != "0",
        Err(_) => false,
    },
};

In other words, this code will still just print disabled backtrace even though we have a RUST_BACKTRACE environment variable:

use std::backtrace::Backtrace;
 
fn main() {
    std::env::set_var("RUST_BACKTRACE", "0");
    println!("{:#?}", Backtrace::capture());
}

But anything else will print a backtrace at run time. This will work:

use std::backtrace::Backtrace;
 
fn main() {
    std::env::set_var("RUST_BACKTRACE", "1");
    println!("{}", Backtrace::capture());
}

Setting it to literally anything else will enable capturing a backtrace:

use std::backtrace::Backtrace;
 
fn main() {
    std::env::set_var("RUST_BACKTRACE", "Hi I'm backtrace");
    println!("{}", Backtrace::capture());
}

Now that it is enabled, the output will look something like what we see in the following example. (It will depend on your operating system, of course.) Here is what the Playground displays:

   0: playground::main
             at ./src/main.rs:5:20
   1: core::ops::function::FnOnce::call_once
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library
             /core/src/ops/function.rs:507:5
   2: std::sys_common::backtrace::__rust_begin_short_backtrace
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/
             src/sys_common/backtrace.rs:121:18
   3: std::rt::lang_start::{{closure}}
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/
             src/rt.rs:166:18
   4: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for
   &F>::call_once
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library
             /core/src/ops/function.rs:606:13
   5: std::panicking::try::do_call
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panicking.rs:483:40
   6: std::panicking::try
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panicking.rs:447:19
   7: std::panic::catch_unwind
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panic.rs:137:14
   8: std::rt::lang_start_internal::{{closure}}
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/rt.rs:148:48
   9: std::panicking::try::do_call
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panicking.rs:483:40
  10: std::panicking::try
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panicking.rs:447:19
  11: std::panic::catch_unwind
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/panic.rs:137:14
  12: std::rt::lang_start_internal
             At /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/rt.rs:148:20
  13: std::rt::lang_start
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std
             /src/rt.rs:165:17
  14: main
  15: __libc_start_main
  16: _start

That was pretty easy. Now let’s finish up with an example that combines both a panic hook and a backtrace.

The Backtrace struct also has a method called status() that returns an enum called a BacktraceStatus. Instead of just printing out the Backtrace struct, we can also match on the BacktraceStatus enum. The enum is quite simple but is interesting for two reasons:

#[non_exhaustive]
pub enum BacktraceStatus {
    Unsupported,
    Disabled,
    Captured,
}

Here is the example:

use std::{
    backtrace::{Backtrace, BacktraceStatus::*},
    panic,
};
 
fn main() {
    panic::set_hook(Box::new(|_| {
        println!("Panicked! Trying to get a backtrace...");
        let backtrace = Backtrace::capture();                            
        match backtrace.status() {                                       
            Disabled => println!("Backtrace isn't enabled, sorry"),
            Captured => println!("Here's the backtrace!!\n{backtrace}"),
            Unsupported => println!("No backtrace possible, sorry"),     
            // Do some database shutting down stuff 
        }
    }));
 
    std::env::set_var("RUST_BACKTRACE", "0");                            
    panic!();
}

When a panic happens, we’ll try to capture a backtrace.

The code matches on the BacktraceStatus enum to see whether a backtrace has been enabled.

It’s pretty rare to find an architecture that wouldn’t support a backtrace, but if so, we would see this message.

Finally, we enable or disable the backtrace.

And the output is

Panicked! Trying to get a backtrace...
Backtrace isn't enabled, sorry

Rust’s precise error handling means that backtraces aren’t used as much as backtraces (also known as stack traces) in other languages because you don’t usually have to sift through a backtrace to find out what has gone wrong in your code. But the option is always there if you need the extra insight.

21.4 The standard library prelude

The prelude in the standard library is the reason why you don’t have to write things like use std::vec::Vec to use a Vec or std::result::Result::Ok() instead of Ok(). You can see all the items in the documentation (http://mng.bz/2768) and will already know almost all of them.

There is an attribute called #![no_implicit_prelude] that disables the prelude. Let’s give it a try and watch just how hard it becomes to write even the simplest of code:

#![no_implicit_prelude]
fn main() {
    let my_vec = vec![8, 9, 10];
    let my_string = String::from("This won't work");
    println!("{my_vec:?}, {my_string}");
}

Now Rust has no idea what you are trying to do:

error: cannot find macro `println` in this scope
 --> src/main.rs:5:5
  |
5 |     println!("{:?}, {}", my_vec, my_string);
  |     ^^^^^^^
 
error: cannot find macro `vec` in this scope
 --> src/main.rs:3:18
  |
3 |     let my_vec = vec![8, 9, 10];
  |                  ^^^
 
error[E0433]: failed to resolve: use of undeclared type or module `String`
 --> src/main.rs:4:21
  |
4 |     let my_string = String::from("This won't work");
  |                     ^^^^^^ use of undeclared type or module `String`
 
error: aborting due to 3 previous errors

For this simple code, you need to tell Rust to use the extern (external) crate called std and then the items you want. Here is everything we have to do just to create a Vec and a String and print it:

#![no_implicit_prelude]
 
extern crate std;            
use std::convert::From;
 
fn main() {                  
    let my_vec = std::vec![8, 9, 10];
    let my_string = std::string::String::from("This won't work");
    std::println!("{my_vec:?}, {my_string}");
}

We told Rust with #![no_implicit_prelude] that we won’t be bringing in anything from std, so we have to let the compiler know again that we will use it.

To write even this simple code, we need the vec! macro, String, From (to convert from a &str to a String), and println! to print.

Now it finally works, printing [8, 9, 10], This won't work. So you can see why Rust has a prelude—it would be a horrible experience without it.

You might be wondering why we haven’t see the extern keyword before. It’s because you don’t need it that much anymore. Up until 2018, you had to use this keyword when bringing in an external crate. So, to use rand in the past, you had to write extern crate rand;, followed by use statements for whatever else you wanted to bring into scope. But the Rust compiler doesn’t need this help anymore; you can just use use, and it knows where to find it. So you almost never need extern crate anymore. But in other people’s Rust code, you might still see it from time to time.

21.5 Other macros

We are getting close to the next chapter, where we will learn to write our own macros. But there are still quite a few macros inside the standard library that we haven’t taken a look at yet, so let’s learn them first. As is the case with the other macros we have used, they are all extremely easy to use and have a bit of a magical feel to them (until we learn in the next chapter how they work internally, that is).

21.5.1 unreachable!

The unreachable! macro is kind of like todo! except it’s for code that will never be executed. Maybe you have a match in an enum that you know will never choose one of the arms, so the code can never be reached. If that’s so, you can write unreachable! so the compiler knows that it can ignore that part.

For example, let’s say you are using an external crate for a financial tool, and it includes a big enum with all the major banks. We’re going to do a match on it, but as we look at the list we notice something:

enum Bank {
    BankOfAmerica,
    Hsbc,
    Citigroup,
    DeutscheBank,
    TorontoDominionBank,
    SiliconValleyBank
    // And so on...
}

Silicon Valley Bank is no more! We are 100% sure that customers will never choose it, so we don’t want to mark this variant as todo! or unimplemented!. We’re never going to implement it. This is definitely a case for the unreachable! macro:

enum Bank {
    BankOfAmerica,
    Hsbc,
    Citigroup,
    DeutscheBank,
    TorontoDominionBank,
    SiliconValleyBank
    // And so on...
}
 
 
fn get_swift_code(bank: &Bank) -> &'static str {
    use Banks::*;
    match bank {
        BankOfAmerica => "BOFAUS3N",
        Hsbc => "HSBCHKHHXXX",
        Citigroup => "CITIUS33XXX",
        DeutscheBank => "DEUTINBBPBC",
        TorontoDominionBank => "TDOMCATTTOR",
        SiliconValleyBank => unreachable!()
    }
}

Another case for unreachable! is when the compiler can’t see something that we can. The following example shows a function that gives a random number from 0 to 3 as a usize, followed by another one called human_readable_rand_num() that gives a human-readable version of the output: zero instead of 0, one instead of 1, and so on. We are 100% certain that the function will never see any number that isn’t in the range of 0..=3, but the compiler doesn’t know this. The unreachable! macro is perfect in this situation:

use rand::{thread_rng, Rng};
 
fn zero_to_three() -> usize {
    let mut rng = thread_rng();
    rng.gen_range(0..=3)
}
 
fn human_readable_rand_num() -> &'static str {
    match zero_to_three() {
        0 => "zero",
        1 => "one",
        2 => "two",
        3 => "three",
        _ => unreachable!(),
    }
}

unreachable! is nice for others reading your code as a reminder of how code works: it’s an assertion that something will never happen. You have to be sure that the code is actually unreachable, though. If the compiler ever calls unreachable!, the program will panic. Just like todo!, the responsibility is on us to make sure that the macro is never called.

On a related note, you’ll see the word unreachable (not the macro unreachable!) when the compiler can determine that some code will never be run. Here is a quick example:

fn main() {
    let true_or_false = true;
 
    match true_or_false {
        true => println!("It's true"),
        false => println!("It's false"),
        true => println!("It's true"),
    }
}

Here, the compiler knows that the match will never reach the third line because it has already checked for both of the possible patterns: true and false.

warning: unreachable pattern
 --> src/main.rs:7:9
  |
7 |         true => println!("It's true"),
  |         ^^^^
  |

You’ll see this “unreachable pattern” warning unexpectedly sometimes. The following code creates an enum representing the four seasons and a function that matches on each season. Take a close look at the code and see whether you can tell why the compiler is going to warn us that there are unreachable parts of the code:

pub enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
}
 
pub fn handle_season(season: Season) {
    use Season::*;
    match season {
        Spring => println!("Spring"),
        summer => println!("Summer"),
        Autumn => println!("Autumn"),
        Winter => println!("Winter")
    }
}

Now let’s take a close look at the output:

warning: unreachable pattern
  --> src/lib.rs:13:9
   |
12 |         summer => println!("Summer"),
   |         ------ matches any value
13 |         Autumn => println!("Autumn"),
   |         ^^^^^^ unreachable pattern
   |
   = note: `#[warn(unreachable_patterns)]` on by default
 
warning: unreachable pattern
  --> src/lib.rs:14:9
   |
12 |         summer => println!("Summer"),
   |         ------ matches any value
13 |         Autumn => println!("Autumn"),
14 |         Winter => println!("Winter")
   |         ^^^^^^ unreachable pattern

Can you see it? We made a typo when we tried to match on Summer, writing summer instead. Instead of matching on an enum variant, we created a wildcard variable called summer that will match on anything. And since it matches on anything, the code will never reach the Summer and Winter parts of the match statement.

21.5.2 column!, line!, file!, and module_path!

These four macros are incredibly easy and are just used to display the current location in the code. Here they are together:

These can be useful when generating error input or even just to print out hints for yourself that there are oddities to check out in the code. We’ll use the previous Bank enum example again to illustrate this. In the following code, we are starting to put some modules together to handle bank customers, and this time, we think there might be some Silicon Valley Bank customers still somewhere in the system. Instead of panicking, though, we’ll print out a warning and give the location in the code to make it easy to find and devise a fix:

pub mod input_handling {
 
    pub struct User {
        pub name: String,
        pub bank: Bank,
    }
 
    #[derive(Debug, Clone, Copy)]
    pub enum Bank {
        BankOfAmerica,
        Hsbc,
        Citigroup,
        DeutscheBank,
        TorontoDominionBank,
        SiliconValleyBank,
    }
 
    pub mod user_input {
        use crate::input_handling::{Bank, User};
        pub fn handle_user_input(user: &User) -> Result<(), ()> {
            match user.bank {
                Bank::SiliconValleyBank => {
                    println!(
                        "Darn it, looks like we have to handle this variant
                         even though Silicon Valley Bank doesn't exist
                         anymore: {}:{}:{}:{}",
                        module_path!(),
                        file!(),
                        column!(),
                        line!()
                    );
                    Ok(())
                }
                other_bank => {
                    println!("{other_bank:?}, no problem");
                    Ok(())
                }
            }
        }
    }
}
 
use crate::input_handling::{user_input::handle_user_input, Bank, User};
 
fn main() {
    let user = User {
        name: "SomeUser".to_string(),
        bank: Bank::SiliconValleyBank,
    };
    handle_user_input(&user).unwrap();
 
    let user2 = User {
        name: "SomeUser2".to_string(),
        bank: Bank::TorontoDominionBank,
    };
    handle_user_input(&user2).unwrap();
}

It prints

Darn it, looks like we have to handle this variant even though Silicon
Valley Bank doesn't exist anymore: playground::input_handling::user
_input:src/main.rs:25:28
TorontoDominionBank, no problem

21.5.3 thread_local!

This macro is similar to the lazy_static! macro that we saw in the lazy_static crate, except that the global content is local to the thread in which it is contained. Or rather, it might be more accurate to say that lazy_static! is similar to thread_ local! because thread_local! is much, much older—it was released along with Rust version 1.0.0!

In any case, when this macro is used, you can create a static that will have the same initial value in every thread in which it is used. The value can then be accessed with a method called .with() that gives access to the value inside within a closure.

The easiest way to see how this works is with a simple example that compares it with the lazy_static behavior that we already know. The following code contains some test functions and should be run with cargo test -- --nocapture so that you can see the output. Remember: each test runs on its own thread!

use std::cell::RefCell;
use std::sync::Mutex;
 
lazy_static::lazy_static! {
    static ref INITIAL_VALUE: Mutex<i32> = Mutex::new(10);        
}
 
thread_local! {
    static LOCAL_INITIAL_VALUE: RefCell<i32> = RefCell::new(10);  
}
 
#[test]                                                           
fn one() {
    let mut lock = INITIAL_VALUE.lock().unwrap();
    println!("Test 1. Global value is {lock}");
    *lock += 1;
    println!("Test 1. Global value is now {lock}");
 
    LOCAL_INITIAL_VALUE.with(|cell| {
        let mut lock = cell.borrow_mut();
        println!("Test 1. Local value is {lock:?}");
        *lock += 1;
        println!("Test 1. Local value is now {lock:?}\n");
    });
}
 
#[test]
fn two() {
    let mut lock = INITIAL_VALUE.lock().unwrap();
    println!("Test 2. Global value is {lock}");
    *lock += 1;
    println!("Test 2. Global value is now {lock}");
 
    LOCAL_INITIAL_VALUE.with(|cell| {
        let mut lock = cell.borrow_mut();
        println!("Test 2. Local value is {lock:?}");
        *lock += 1;
        println!("Test 2. Local value is now {lock:?}\n");
    });
}
 
#[test]
fn three() {
    let mut lock = INITIAL_VALUE.lock().unwrap();
    println!("Test 3. Global value is {lock}");
    *lock += 1;
    println!("Test 3. Global value is now {lock}");
 
    LOCAL_INITIAL_VALUE.with(|cell| {
        let mut lock = cell.borrow_mut();
        println!("Test 3. Local value is {lock:?}");
        *lock += 1;
        println!("Test 3. Local value is now {lock:?}\n");
    });
}

This INITIAL_VALUE is accessible to all threads. We have to wrap it in a thread-safe Mutex or RwLock.

However, LOCAL_INITIAL_VALUE is a static that is local to each thread—no need for a Mutex! A regular RefCell or Cell works just fine.

Now, we have three tests, each of which does the exact same thing. Each test increments INITIAL_VALUE by 1 and prints it and then increments LOCAL_INITIAL_VALUE by 1 and prints it.

Each test is running on its own thread, so the order will always be different, but the output will look something like this:

running 3 tests
Test 3. Global value is: 10
Test 3. Global value is now: 11
Test 3. Local value is 10
Test 3. Local value is now 11
 
Test 1. Global value is: 11
Test 1. Global value is now: 12
Test 1. Local value is 10
Test 1. Local value is now 11
 
Test 2. Global value is: 12
Test 2. Global value is now: 13
Test 2. Local value is 10
Test 2. Local value is now 11

As you can see, by the end of all three tests, the INITIAL_VALUE (the global value) is now 13. But the LOCAL_INITIAL_VALUE (the thread-local value) starts at 10 inside each thread, and the other tests don’t affect it.

If you check the documentation for LocalKey (the type created by the macro; https://doc.rust-lang.org/std/thread/struct.LocalKey.html), you’ll see a lot of methods that look like the methods for Cell and RefCell. These methods were experimental for quite some time, but were stabilized shortly before the publication of this book in Rust 1.73!

21.5.4 cfg!

We know that you can use attributes like #[cfg(test)] and #[cfg(windows)] to tell the compiler what to do in certain cases. When you have the #[test] attribute, Rust will run the code when running a test. And when you use windows, it will run the code if the user is using Windows. But maybe you just want to change one tiny bit of code depending on the configuration. That’s when this macro is useful. It returns a bool:

fn main() {
    let helpful_message = if cfg!(target_os = "windows") {
        "backslash"
    } else {
        "slash"
    };
    println!("...then type the directory name followed by a
    {helpful_message}. Then you...");
} 

This will print differently, depending on your system. The Rust Playground runs on Linux, so it will print

...then in your hard drive, type the directory name followed by a slash.
Then you...

The cfg! macro works for any kind of configuration. Here is an example of a function that runs differently when you use it inside a test. We have a UserFile enum that can hold either real data (a File) or test data (a String). If this code is run inside main(), the open_file() function will open up the main.rs file and pass it on. If run inside a test, though, it will simply create a dummy String and pass that on instead. Try running this code with Run and with Test in the Playground (or cargo run and cargo test on your computer) to see the difference in behavior:

use std::fs::File;
use std::io::Read;
 
#[derive(Debug)]
enum UserFile {
    Real(File),
    Test(String),
}
 
fn open_file() -> UserFile {
    if cfg!(test) {
        UserFile::Test(String::from("Just a test file"))
    } else {
        UserFile::Real(File::open("src/main.rs").unwrap())
    }
}
 
fn get_file_content() -> String {
    let mut content = String::new();
    let file = open_file();
    match file {
        UserFile::Real(mut f) => {
            f.read_to_string(&mut content).unwrap();
            content
        }
        UserFile::Test(s) => s,
    }
}
 
#[test]
fn test_file() {
    let content = get_file_content();
    println!("Content is: {content}");
    assert_eq!(content, "Just a test file");
}
 
fn main() {
    let content = get_file_content();
    println!("{content}");
} 

When this code is run with cargo run, it will print the entire content of the main.rs file, but during a test with cargo test -- --nocapture, it will simply print the following:

running 1 test
Content: Just a test file
test test_file ... ok

Hopefully, you found this tour of the standard library relaxing and fruitful. There are a lot of hidden gems in the standard library that even experienced Rust users haven’t used before, so feel free to do your own tour and see what you can find. There are also a lot of experimental methods that might be stabilized one day, so check the tracking problems to see what work is being done if you find a method that you like in particular. And remember, experimental doesn’t mean unsafe! An experimental method is just a method that might be stabilized one day or might be thrown out if it doesn’t make sense to stabilize it.

After these two relaxing chapters, there is only one difficult chapter left to go, and it’s the next one: making your own macros!

Summary