[The Rust Programming Language] 7. Managing Growing Projects with Packages, Crates, and Modules

7. Managing Growing Projects with Packages, Crates, and Modules

Rust has a number of features that allow you to manage your code’s organization, including which details are exposed, which details are private, and what names are in each scope in your programs. These features, sometimes collectively referred to as the module system, include:

  • Packages: A Cargo feature that lets you build, test, and share crates

  • Crates: A tree of modules that produces a library or executable

  • Modules and use: Let you control the organization, scope, and privacy of paths

  • Paths: A way of naming an item, such as a struct, function, or module

7.1 Packages and Crates

A crate is a binary or library. The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate (we’ll explain modules in depth in the “Defining Modules to Control Scope and Privacy” - https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html section).

A package is one or more crates that provide a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.

Several rules determine what a package can contain. A package must contain zero or one library crates, and no more. It can contain as many binary crates as you’d like, but it must contain at least one crate (either library or binary).

Let’s walk through what happens when we create a package. First, we enter the command cargo new:

1
2
3
4
5
6
7
8
9
$ cargo new my-project
Created binary (application) `my-project` package

$ ls my-project
Cargo.toml
src

$ ls my-project/src
main.rs

When we entered the command, Cargo created a Cargo.toml file, giving us a package. Looking at the contents of Cargo.toml, there’s no mention of src/main.rs because Cargo follows a convention that src/main.rs is the crate root of a binary crate with the same name as the package. Likewise, Cargo knows that if the package directory contains src/lib.rs, the package contains a library crate with the same name as the package, and src/lib.rs is its crate root. Cargo passes the crate root files to rustc to build the library or binary.

/ toml
1
2
3
4
5
6
7
8
9
[package]
name = "my-project"
version = "0.1.0"
authors = ["Benjamin CloudoLife <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Run rustc:

1
2
3
$ cargo rustc  
Compiling my-project v0.1.0 (/Users/cloudolife/my-project)
Finished dev [unoptimized + debuginfo] target(s) in 8.05s

Run binary:

1
2
$ target/debug/my-project
Hello, world!

Here, we have a package that only contains src/main.rs, meaning it only contains a binary crate named my-project. If a package contains src/main.rs and src/lib.rs, it has two crates: a library and a binary, both with the same name as the package. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.

A crate will group related functionality together in a scope so the functionality is easy to share between multiple projects.

7.2 Defining Modules to Control Scope and Privacy

Namely paths that allow you to name items; the use keyword that brings a path into scope; and the pub keyword to make items public. We’ll also discuss the as keyword, external packages, and the glob operator. For now, let’s focus on modules!

Modules let us organize code within a crate into groups for readability and easy reuse. Modules also control the privacy of items, which is whether an item can be used by outside code (public) or is an internal implementation detail and not available for outside use (private).

To structure our crate in the same way that a real restaurant works, we can organize the functions into nested modules. Create a new library named restaurant by running cargo new --lib restaurant; then put the code in Listing 7-1 into src/lib.rs to define some modules and function signatures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}

7.3 Paths for Referring to an Item in the Module Tree

To show Rust where to find an item in a module tree, we use a path in the same way we use a path when navigating a filesystem. If we want to call a function, we need to know its path.

A path can take two forms:

  • An absolute path starts from a crate root by using a crate name or a literal crate.

  • A relative path starts from the current module and uses self, super, or an identifier in the current module.

Both absolute and relative paths are followed by one or more identifiers separated by double colons (::).

Our preference is to specify absolute paths because it’s more likely to move code definitions and item calls independently of each other.

Exposing Paths with the pub Keyword

Modules aren’t useful only for organizing your code. They also define Rust’s privacy boundary: the line that encapsulates the implementation details external code isn’t allowed to know about, call, or rely on. So, if you want to make an item like a function or struct private, you put it in a module.

The way privacy works in Rust is that all items (functions, methods, structs, enums, modules, and constants) are private by default. Items in a parent module can’t use the private items inside child modules, but items in child modules can use the items in their ancestor modules. The reason is that child modules wrap and hide their implementation details, but the child modules can see the context in which they’re defined. To continue with the restaurant metaphor, think of the privacy rules as being like the back office of a restaurant: what goes on in there is private to restaurant customers, but office managers can see and do everything in the restaurant in which they operate.

Rust chose to have the module system function this way so that hiding inner implementation details is the default. That way, you know which parts of the inner code you can change without breaking outer code. But you can expose inner parts of child modules’ code to outer ancestor modules by using the pub keyword to make an item public.

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting { // Remember to make pub
pub fn add_to_waitlist() {} // Remember to make pub
}
}

pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();

// Relative path
front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

We can also construct relative paths that begin in the parent module by using super at the start of the path.

1
2
3
4
5
6
7
8
9
10
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}

fn cook_order() {}
}

Making Structs and Enums Public

We can also use pub to designate structs and enums as public, but there are a few extra details. If we use pub before a struct definition, we make the struct public, but the struct’s fields will still be private. We can make each field public or not on a case-by-case basis.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
}

In contrast, if we make an enum public, all of its variants are then public. We only need the pub before the enum keyword,

1
2
3
4
5
6
7
8
9
10
11
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}

7.4 Bringing Paths into Scope with the use Keyword

We can bring a path into a scope once and then call the items in that path as if they’re local items with the use keyword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/lib.rs

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;
// Or use relative path.
// use self::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();

Creating Idiomatic use Paths

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/lib.rs

mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
add_to_waitlist();
add_to_waitlist();
add_to_waitlist();
}

Providing New Names with the as Keyword

There’s another solution to the problem of bringing two types of the same name into the same scope with use: after the path, we can specify as and a new local name, or alias, for the type.

1
2
3
4
5
6
7
8
9
10
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
}

fn function2() -> IoResult<()> {
// --snip--
}

Re-exporting Names with pub use

When we bring a name into scope with the use keyword, the name available in the new scope is private. To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub and use. This technique is called re-exporting because we’re bringing an item into scope but also making that item available for others to bring into their scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

Using External Packages

Adding rand as a dependency in Cargo.toml tells Cargo to download the rand package and any dependencies from crates.io and make rand available to our project.

/ toml
1
2
[dependencies]
rand = "0.8.3"

Then, to bring rand definitions into the scope of our package, we added a use line starting with the name of the crate, rand, and listed the items we wanted to bring into scope.

1
2
3
4
5
use rand::Rng;

fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}

Using Nested Paths to Clean Up Large use Lists

We do this by specifying the common part of the path, followed by two colons, and then curly brackets around a list of the parts of the paths that differ, as shown in Listing 7-18.

1
2
3
4
5
6
// --snip--
use std::{cmp::Ordering, io};

// Equal to follow:
// use std::cmp::Ordering;
// use std::io;

We can use a nested path at any level in a path, which is useful when combining two use statements that share a subpath.

1
2
3
4
5
use std::io::{self, Write}; // This line brings std::io and std::io::Write into scope.

// Equal to follow:
// use std::io;
// use std::io::Write;

The Glob Operator

If we want to bring all public items defined in a path into scope, we can specify that path followed by *, the glob operator:

1
use std::collections::*;

Be careful when using the glob operator! Glob can make it harder to tell what names are in scope and where a name used in your program was defined.

The glob operator is often used when testing to bring everything under test into the tests module; we’ll talk about that in the “How to Write Tests” - https://doc.rust-lang.org/book/ch11-01-writing-tests.html#how-to-write-tests section in Chapter 11 - https://doc.rust-lang.org/std/prelude/index.html#other-preludes. The glob operator is also sometimes used as part of the prelude pattern: see the standard library documentation for more information on that pattern.

7.5 Separating Modules into Different Files

Using a semicolon(;) after mod front_of_house rather than using a block({}) tells Rust to load the contents of the module from another file with the same name as the module.

1
2
3
4
5
6
7
8
9
10
11
// src/lib.rs

mod front_of_house; // With a semicolon(;), load the contents of the module from another file with the same name as the module

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
1
2
3
4
5
// src/front_of_house.rs

pub mod hosting {
pub fn add_to_waitlist() {}
}

To continue with our example and extract the hosting module to its own file as well, we change src/front_of_house.rs to contain only the declaration of the hosting module:

1
2
3
// src/front_of_house.rs

pub mod hosting; // Remember to use pub to re-export module name.

Then we create a src/front_of_house directory and a file src/front_of_house/hosting.rs to contain the definitions made in the hosting module:

1
2
3
// src/front_of_house/hosting.rs

pub fn add_to_waitlist() {}

The mod keyword declares modules, and Rust looks in a file with the same name as the module for the code that goes into that module.

References

[1] The Rust Programming Language - The Rust Programming Language - https://doc.rust-lang.org/book/title-page.html

[2] Managing Growing Projects with Packages, Crates, and Modules - The Rust Programming Language - https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html