[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 | cargo new my-project |
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.r
s 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.
1 | [package] |
Run rustc:
1 | cargo rustc |
Run binary:
1 | target/debug/my-project |
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 | mod front_of_house { |
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 | mod front_of_house { |
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 | fn serve_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 | mod back_of_house { |
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 | mod back_of_house { |
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 | // src/lib.rs |
Creating Idiomatic use Paths
1 | // src/lib.rs |
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 | use std::fmt::Result; |
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 | mod front_of_house { |
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.
1 | [dependencies] |
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 | use rand::Rng; |
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 | // --snip-- |
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 | use std::io::{self, Write}; // This line brings std::io and std::io::Write into scope. |
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 | // src/lib.rs |
1 | // src/front_of_house.rs |
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 | // src/front_of_house.rs |
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 | // src/front_of_house/hosting.rs |
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.