Managing Growing PRojects with Packages, Crates, and Modules1
To run the program:
$ cargo run --bin growing-projects
Compiling growing-projects v0.1.0 ...
The programs I wrote so far have been one module, in one file.
As a project grows, I organize code by:
- Splitting it into multiple modules
- And then multiple files.
A package can contain multiple binary cartes and optionally one library crate.
As a package grows, I extract parts into separate crates that become external dependencies.
NOTE: For very large projects comprising a set of interrelated packages that evolve together, Cargo also provides workspaces, but that isn't covered until Chapter 14.
In general, the features that are supported to organize code include:
- Packages: A Cargo feature that lets you build, test, and share crates.
- Crates: A tree of modules that produces a libray or executable.
- Modules and
use
: Let you control the organization, scope, privacy of paths. - Paths: A way of naming an item, such as a struct, function, or module.
A crate is the smallest amount of code that the Rust compiler considers at a time.
For example if you run:
rustc main.rust
... the compiler considers that single file to be a crate.
Crates can contain modules, and modules may be defined in other files.
A crate comes in one of two forms: a binary crate or a library crate:
- Binary crates are programs that are compiled and run, i.e. a CLI or server.
- Each must have a function called
main
.
- Each must have a function called
- Library crates don't have a
main
, and don't compile to an executable.- Instead, they define functionality shared with multiple projects.
NOTE: In practice, the word crate often means library crate.
A package is a bundle of one or more crates that provide a set of
functionality. A package contains a Cargo.toml
2 file that describes how
to build those crates. Cargo is a package that contains the binary crate for
the command-line tool I've been using up until this point.
NOTE: Cargo also provides a library crate that other packages can use!
When you create a new package, i.e. with cargo new
, which creates a:
Cargo.toml
configuration file.- A crate root file, either
src/main.rs
orsrc/lib.rs
.
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
# If cargo new my-project --lib, this file would be main.lib
$ ls my-project/src
main.rs
NOTE: Remember that crates can only contain a single library crate.
However multiple binary creates are allowed by creating a
src/bin
:src/ bin/ a.rs b.rs
The module system is roughly:
- Paths allow you to name items
- The
use
keyword brings a path into scope - The
pub
keyword makes items public
The typical workflow is:
-
The compiler starts at the root crate's crate root (typically
src/{lib,main}.rs
). -
In the crate root, you can declare new modules. Let's say the
garden
module:-
It will look inline (in the crate root) for:
mod garden { // ... }
or (these examples are semantically the same):
mod garden; // ...
-
If not found, it will look in
src/garden.rs
. -
If not found, it will look in
src/garden/mod.rs
.
-
-
In any other file (other than crate root), declare submodules, i.e.
vegetables
:- Inline, directly following
mod vegetables;
or withinmod vegetables {}
. - If not found, in the file
src/garden/vegetables.rs
. - If not found, in the ffile
src/garden/vegetables/mod.rs
.
- Inline, directly following
-
Once a module is part of your crate, and it's visible to you, you refer to it:
// For example, an `Asparagus` type in the garden/vegetables module: crate::garden::vegetables::Asparagus
-
Code within a module is private by default.
- To make a module public, declare it with
pub mod
instead ofmod
. - To make an item within a public module public, use
pub
, i.e.pub enum
.
- To make a module public, declare it with
-
Using the
use
keyword to create scope shortcuts:use crate::garden::vegetables::Asparagus; // You can now refer directly to `Asparagus`.
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
To write a library crate that provides functionality of a restaurant:
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() {}
}
}
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
To navigate the module tree (similar to a filesystem), we need a path:
-
An absolute path is the full path starting from a crate root
- For an external crate, it's
{name}::...
. - For the current create, it's
crate::...
.
- For an external crate, it's
-
A relative path starts from the current module and uses either:
self
super
(similar to../
in a filesystem path)- an identifier in the current module
The preference in the Rust Book is to:
-
Use absolute paths to make refactors easy.
-
Import the module (not the full path) to call functions:
// BAD. use crate::front_of_house::hosting::add_to_waitlist { pub fn eat_at_restaurant() { add_to_waitlist(); } } // GOOD. use crate::front_of_house::hosting { pub fn eat_at_restaurant() { hosting::add_to_waitlist(); } }
-
Use full names, i.e.
front_of_house.rs
overfront_of_house/mod.rs
. -
Import the full path to use structs, enums, and other items:
// GOOD. use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
-
As an exception, use the module name or
as
rename if names would conflict:// GOOD. use std::fmt; use std::io; fn f1() -> fmt::Result { /* ... */ } fn f2() -> io::Result<()> { /* ... */ } // GOOD. use std::fmt::Result; use std::io::Result as IoResult; fn f1() -> Result { /* ... */ } fn f2() -> IoResult<()> { /* ... */ }
Can also use nested paths to clean-up large use
lists:
use std::cmp::Ordering;
use std::io;
// Semantically the same:
use std::{cmp::Ordering, io};
Another example:
use std::io;
use std::io::Write;
// Semanticaly the same:
use std::io::{self, Write};
There is also a glob operator, which is typically used by a tests
module:
// See also: https://doc.rust-lang.org/std/prelude/index.html#other-preludes
use std::collections::*;
Can also write pub use
to re-export code (the book will expand in chapter 14).
For example, using rand
(https://crates.io/crates/rand):
cargo add rand
[dependencies]
rand = "0.8.5"
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Though not specifically covered by this section, some cargo
CLI steps:
# Resolves and adds the "rand" package (from crates.io) to "growing-projects".
cargo add rand -p growing-projects
Footnotes
-
Source: https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html ↩
-
Why this file starts with a uppercase character, just to throw off my file naming schemes, you can read about here. ↩