Intermediate Topics: Modules, Crates, and the Cargo Ecosystem
As your Rust projects grow in complexity, organizing your code becomes paramount for maintainability, reusability, and collaboration. Rust provides a robust module system, managed by its powerful build tool and package manager, Cargo. This chapter will guide you through understanding Rust’s project hierarchy, controlling visibility, and leveraging the rich Cargo ecosystem.
Understanding the Hierarchy: Packages, Crates, and Modules
Rust’s code organization follows a clear hierarchy:
Packages: The highest level of organization. A package is what Cargo builds. It contains:
- A
Cargo.tomlfile that describes the package (metadata, dependencies, etc.). - One or more crates.
- Can contain a binary crate (executable, default
src/main.rs), a library crate (defaultsrc/lib.rs), or both.
- A
Crates: The fundamental compilation unit in Rust.
- A library crate produces a library (e.g.,
.rlibfile). - A binary crate produces an executable.
- Every crate has an implicit crate root module, which is
src/main.rsfor a binary crate orsrc/lib.rsfor a library crate.
- A library crate produces a library (e.g.,
Modules: Organize code within a crate. They allow you to group related functions, structs, enums, etc., and control their visibility (whether they are public or private).
Think of a package as a book, a crate as a chapter within that book, and modules as sections within that chapter.
Modules and mod Keyword
You define a module using the mod keyword.
Declaring Modules
Modules can be declared inline within a file or in separate files/directories.
Inline Modules
// src/main.rs or src/lib.rs
mod front_of_house {
fn serve_food() {
println!("Serving delicious food!");
}
mod hosting { // Nested module
fn add_to_waitlist() {
println!("Added to waitlist.");
}
}
}
fn main() {
// You cannot directly access `serve_food()` here because it's private to `front_of_house`.
// front_of_house::serve_food(); // ERROR!
// front_of_house::hosting::add_to_waitlist(); // ERROR!
}
By default, items within a module are private to that module. To make them accessible from parent modules or other modules, you use the pub keyword.
Separating Modules into Files
For larger projects, it’s common practice to put modules in their own files.
If you declare mod front_of_house; in src/lib.rs or src/main.rs, Rust will look for the code for that module in one of two places:
src/front_of_house.rssrc/front_of_house/mod.rs(the traditional way, now less common in new projects for top-level modules, but still valid for nested modules in subdirectories).
Example: src/lib.rs
// src/lib.rs
pub mod front_of_house; // Declares the module and tells Rust to look for its code
Example: src/front_of_house.rs
// src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {
println!("Adding to waitlist in front of house module!");
}
pub fn seat_at_table() {
println!("Seating at table in front of house module!");
}
}
pub mod serving {
pub fn take_order() {
println!("Taking order in front of house module!");
}
}
Now, if you have src/main.rs that uses this library:
// src/main.rs
use my_restaurant_lib::front_of_house::hosting; // Use the path to the module
fn main() {
hosting::add_to_waitlist(); // Now accessible
// hosting::seat_at_table();
// my_restaurant_lib::front_of_house::serving::take_order();
}
Here, my_restaurant_lib would be the name of your library crate, as defined in Cargo.toml.
Paths for Referring to Items in the Module Tree
To refer to an item within a module, you use its path. Paths can be:
- Absolute Paths: Start from the crate root (
crate::for the current crate, or the crate name for an external crate). - Relative Paths: Start from the current module (
self::for the current module, orsuper::for the parent module).
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {
println!("Patron added to waitlist.");
}
}
}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::front_of_house::hosting::add_to_waitlist(); // Relative path to parent's child
}
fn cook_order() {
println!("Cooking order.");
}
}
fn eat_at_restaurant() {
// Absolute path to a function
crate::front_of_house::hosting::add_to_waitlist();
// Relative path to a function
front_of_house::hosting::add_to_waitlist();
}
fn main() {
eat_at_restaurant();
}
pub and Privacy Rules
Rust’s privacy model is explicit and designed to protect implementation details.
- By default, all items (functions, methods, structs, enums, modules, constants) are private.
- To make an item public, you must explicitly use the
pubkeyword.
pub on Structs and Enums
When you make a struct public, its fields are still private by default. You need to mark individual fields as pub if you want external code to access them directly.
mod back_of_house {
pub struct Breakfast {
pub toast: String, // Public field
seasonal_fruit: String, // Private field
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
fn main() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat"); // OK: toast is public
// meal.seasonal_fruit = String::from("blueberries"); // ERROR: seasonal_fruit is private
println!("I'd like {} toast please", meal.toast);
}
When you make an enum public, all its variants are automatically public. This makes sense, as an enum with private variants would not be very useful.
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
fn main() {
let order1 = back_of_house::Appetizer::Soup; // OK: Soup variant is public
let order2 = back_of_house::Appetizer::Salad; // OK: Salad variant is public
}
The use Keyword
The use keyword brings a path into scope, allowing you to refer to items by shorter names.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Bring the `hosting` module into scope
use crate::front_of_house::hosting;
fn main() {
hosting::add_to_waitlist(); // Now we can call it directly
}
Idiomatic use Paths
- Functions: It’s idiomatic to bring the function’s parent module into scope, rather than the function itself. This helps to avoid name collisions and makes it clear that the function is from a module.
use crate::front_of_house::hosting; // Idiomatic // use crate::front_of_house::hosting::add_to_waitlist; // Less idiomatic for functions - Structs, Enums, Traits: It’s idiomatic to bring the full path, including the item itself, into scope.
use std::collections::HashMap; let mut map = HashMap::new();
Nested Paths for use
You can use nested paths to bring multiple items from the same module into scope with one use statement.
use std::{cmp::Ordering, io}; // Brings Ordering from std::cmp and io from std
use std::collections::{self, HashMap, HashSet}; // Brings collections module itself, HashMap, and HashSet
The as Keyword for Aliases
Use as to rename an item you’re bringing into scope, especially to resolve name conflicts.
use std::fmt::Result;
use std::io::Result as IoResult; // Renames std::io::Result to IoResult
fn function1() -> Result { /* ... */ Ok(()) } // Refers to std::fmt::Result
fn function2() -> IoResult<()> { /* ... */ Ok(()) } // Refers to std::io::Result
pub use for Re-exporting
pub use allows you to make an item available to external code at a different, often more convenient, path. This is key for designing user-friendly public APIs for your libraries.
// src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting; // Re-export `hosting`
fn main() {
hosting::add_to_waitlist(); // This still works internally
// External users of this library can now call:
// my_crate::hosting::add_to_waitlist();
// instead of my_crate::front_of_house::hosting::add_to_waitlist();
}
The Cargo Ecosystem
Cargo is much more than just a build tool; it’s the heart of the Rust ecosystem.
Cargo.toml: The Manifest File
Every Rust package has a Cargo.toml file at its root. This is a TOML (Tom’s Obvious, Minimal Language) file that contains metadata about your project.
[package]
name = "my_app"
version = "0.1.0"
edition = "2024" # The current default edition for new projects
[dependencies]
# External dependencies (crates from crates.io)
rand = "0.8" # For random number generation
serde = { version = "1.0", features = ["derive"] } # For serialization/deserialization
[package]: Contains information about your project.[dependencies]: Lists external crates your project needs. Cargo automatically downloads, compiles, and links these.edition = "2024": Specifies the Rust edition to use for your crate. The Rust 2024 Edition (stabilized with Rust 1.85.0) brings several quality-of-life improvements and language changes, while maintaining backward compatibility with older editions. It introduces features like improvedimpl Traitlifetime capture rules, adjusted temporary scopes forif letand tail expressions, and stricterunsafeusage requirements.
Standard Cargo Commands
cargo new <project_name>: Creates a new Rust project (package) with asrc/main.rs(binary crate) andCargo.toml. Add--libto create a library crate instead.cargo build: Compiles your project. Creates atarget/debugdirectory.cargo run: Compiles and runs your project.cargo check: Compiles your project without producing an executable, useful for quickly checking for errors.cargo test: Runs all tests in your project.cargo add <crate_name>: (Since Rust 1.62.0) A convenient way to add dependencies toCargo.toml.- Example:
cargo add rand - Example:
cargo add serde --features serde_derive(for oldserde_derivefeature, now usually justfeatures = ["derive"])
- Example:
cargo update: Updates dependencies to their latest compatible versions.cargo clean: Removes thetargetdirectory.cargo fmt: Formats your code according to Rust style guidelines (rustfmttool).cargo clippy: Runs a linter to catch common mistakes and idiomatic errors (Clippytool).
Workspaces (for multi-crate projects)
For projects with multiple related crates, a Cargo workspace allows them to share a common Cargo.lock file and target directory, making builds faster and dependency management easier.
my-workspace/
├── Cargo.toml # Workspace manifest
├── core-library/ # A library crate
│ └── Cargo.toml
│ └── src/lib.rs
├── cli-app/ # A binary crate using core-library
│ └── Cargo.toml
│ └── src/main.rs
└── another-tool/ # Another binary crate
└── Cargo.toml
└── src/main.rs
The workspace Cargo.toml looks like:
[workspace]
members = [
"core-library",
"cli-app",
"another-tool",
]
Each member crate will then refer to other local crates in its Cargo.toml:
# cli-app/Cargo.toml
[dependencies]
core-library = { path = "../core-library" } # Local path dependency
Exercises / Mini-Challenges
Exercise 6.1: Restaurant Module Refactor
Take the front_of_house and back_of_house module structure we discussed and organize it into a small library crate named restaurant_menu. Create a separate binary crate that uses this library.
Instructions:
- Create a new package:
cargo new restaurant_project. - Inside
restaurant_project, create a new library crate:cargo new --lib restaurant_menu. This will createrestaurant_project/restaurant_menu/src/lib.rs. - Move the
front_of_houseandback_of_housemodules (and their contents, withpubwhere necessary) intorestaurant_menu/src/lib.rs. - In
restaurant_project/src/main.rs(the binary crate), addrestaurant_menuas a dependency in itsCargo.toml. - In
restaurant_project/src/main.rs, use items fromrestaurant_menu(e.g., calladd_to_waitlist). - Ensure all necessary
pubkeywords are in place for the items you want to access from the binary crate. - Run
cargo runfrom therestaurant_projectroot.
Example restaurant_project/restaurant_menu/src/lib.rs:
// In restaurant_menu/src/lib.rs
pub mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {
println!("Waitlist: New guest added.");
}
}
// Add other front_of_house items here
}
pub mod back_of_house {
pub struct Chef {
name: String,
}
impl Chef {
pub fn new(name: String) -> Chef {
Chef { name }
}
pub fn cook_dish(&self, dish: &str) {
println!("Chef {} is cooking {}.", self.name, dish);
}
}
}
Example restaurant_project/src/main.rs:
// In restaurant_project/src/main.rs
use restaurant_menu::front_of_house::hosting;
use restaurant_menu::back_of_house::Chef;
fn main() {
println!("Welcome to our Rust restaurant!");
hosting::add_to_waitlist();
let head_chef = Chef::new(String::from("Gordon"));
head_chef.cook_dish("Pasta Carbonara");
}
Example restaurant_project/Cargo.toml (for the binary crate):
# In restaurant_project/Cargo.toml
[package]
name = "restaurant_project"
version = "0.1.0"
edition = "2024"
[dependencies]
restaurant_menu = { path = "restaurant_menu" }
Exercise 6.2: Using an External Crate (rand)
Modify your restaurant_project to use the rand crate to simulate a random event or decision.
Instructions:
- Add
randas a dependency torestaurant_project/Cargo.toml(the binary crate).cargo add rand(run fromrestaurant_projectdirectory).
- In
restaurant_project/src/main.rs, userand::thread_rng().gen_range(1..=10)to generate a random number between 1 and 10 (inclusive). - Use an
iformatchstatement based on this random number to:- If the number is 1, print “A new VIP guest arrived, seating immediately!”.
- Otherwise, call
hosting::add_to_waitlist()normally.
// Solution Hint:
/*
// In restaurant_project/src/main.rs
use rand::Rng; // Bring Rng trait into scope
fn main() {
// ... previous code ...
let mut rng = rand::thread_rng();
let lucky_number = rng.gen_range(1..=10);
if lucky_number == 1 {
println!("A new VIP guest arrived, seating immediately!");
} else {
hosting::add_to_waitlist();
}
}
*/
Exercise 6.3: Create a Small Workspace
Convert your restaurant_project into a Cargo workspace containing both the restaurant_menu library and the restaurant_project binary.
Instructions:
- Rename
restaurant_projectdirectory torestaurant-workspace. - Move the contents of the original
restaurant_projectinto a new subdirectory namedapp. So you’ll have:restaurant-workspace/ ├── app/ │ ├── Cargo.toml │ └── src/main.rs └── restaurant_menu/ ├── Cargo.toml └── src/lib.rs - Create a
Cargo.tomlfile at the root ofrestaurant-workspace. This will be your workspace manifest.# In restaurant-workspace/Cargo.toml [workspace] members = [ "app", "restaurant_menu", ] - Update
app/Cargo.tomlto correctly referencerestaurant_menuas a local path dependency within the workspace.# In restaurant-workspace/app/Cargo.toml [package] name = "app" # or whatever you named it version = "0.1.0" edition = "2024" [dependencies] restaurant_menu = { path = "../restaurant_menu" } rand = "0.8" # If you added rand in previous exercise - Run
cargo checkorcargo runfrom therestaurant-workspaceroot directory to verify everything compiles and runs.