Table of Contents
- What Are Crates?
- 1.1 Binary Crates
- 1.2 Library Crates
- 1.3 The Crate Root
- Modules: Organizing Code Within a Crate
- Visibility: Controlling Access with
pub - Paths: Referencing Items in Modules
- 4.1 Absolute Paths
- 4.2 Relative Paths
- Simplifying Paths with
useDeclarations- 5.1 Importing Items
- 5.2 Renaming with
as - 5.3 Glob Imports with
*
- Nested Modules: Building Hierarchies
- Crate Organization Best Practices
- Conclusion
- References
What Are Crates?
At the highest level, a crate is Rust’s smallest compilation unit. Think of it as a package of code that the Rust compiler processes independently. Crates can be compiled into either executables (binary crates) or reusable libraries (library crates). Every Rust project is a crate, and larger projects often consist of multiple crates working together.
Binary Crates
A binary crate is an executable program. It must contain a main function, which serves as the entry point of the program. When you run cargo new my_project, Cargo creates a binary crate by default, with src/main.rs as the crate root.
Example:
cargo new my_binary_crate # Creates a binary crate
cd my_binary_crate
cat src/main.rs # Contains `fn main() { println!("Hello, world!"); }`
Library Crates
A library crate is a collection of reusable code that cannot be executed on its own. It lacks a main function and is designed to be depended on by other crates (binary or library). To create a library crate, use cargo new --lib my_library_crate:
cargo new --lib my_library_crate # Creates a library crate
cd my_library_crate
cat src/lib.rs # Contains example test code (no `main` function)
Library crates are defined by their public API, which other crates can access using use declarations.
The Crate Root
Every crate has a crate root: the source file that the Rust compiler starts processing when compiling the crate. For binary crates, the root is src/main.rs; for library crates, it’s src/lib.rs.
The crate root defines the crate’s module hierarchy. All other modules in the crate are nested under the crate root, either inline or in separate files.
Modules: Organizing Code Within a Crate
While crates group code at the project level, modules organize code within a crate. Modules act as containers for functions, structs, enums, traits, and even other modules, allowing you to group related functionality and control access to code.
Defining Modules
Modules are declared with the mod keyword, followed by a name and a block of code. Modules can be defined inline (directly in the crate root or another module) or in separate files.
Inline Modules
Here’s an example of an inline module in src/lib.rs (a library crate):
// src/lib.rs
mod math { // Define a module named `math`
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
}
In this case, math is a module containing two functions, add and subtract.
Module Structure: Files and Directories
For larger modules, defining code inline becomes unwieldy. Rust allows you to split modules into separate files and directories, following these conventions:
-
Single-file modules: A module named
mathcan be defined insrc/math.rs. To include it in the crate root, usemod math;(no block—Rust infers the module’s code is inmath.rs). -
Multi-file modules (directories): For nested modules, use a directory named after the module (e.g.,
src/math/), with amod.rsfile (or, in Rust 2018+, a file named after the module, e.g.,src/math.rs).
Example: Organizing a math module with submodules arithmetic and geometry:
src/
├── lib.rs # Crate root
└── math/ # Directory for the `math` module
├── mod.rs # Root of the `math` module (optional in Rust 2018+)
├── arithmetic.rs # Submodule `math::arithmetic`
└── geometry.rs # Submodule `math::geometry`
In src/lib.rs, declare the math module to load it from the directory:
// src/lib.rs
pub mod math; // `pub` makes `math` accessible outside the crate root
In src/math/mod.rs, declare the submodules:
// src/math/mod.rs
pub mod arithmetic; // Expose `arithmetic` as part of `math`'s public API
pub mod geometry; // Expose `geometry` as part of `math`'s public API
Now arithmetic.rs and geometry.rs can contain their respective code:
// src/math/arithmetic.rs
pub fn add(a: i32, b: i32) -> i32 { // `pub` makes `add` public
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
// src/math/geometry.rs
pub fn area_of_circle(radius: f64) -> f64 {
std::f64::consts::PI * radius * radius
}
Visibility: Controlling Access with pub
By default, all items (functions, structs, modules, etc.) in Rust are private: they can only be accessed by code within the same module or its parent modules. To make an item accessible outside its module, mark it with the pub keyword.
pub Basics
The pub keyword exposes an item to parent modules and external crates (if the parent module is also public).
Example:
// src/lib.rs
pub mod math { // `math` is public (exposed to the crate's public API)
pub mod arithmetic { // `arithmetic` is public (exposed via `math`)
pub fn add(a: i32, b: i32) -> i32 { // `add` is public
a + b
}
fn multiply(a: i32, b: i32) -> i32 { // Private: only accessible in `arithmetic`
a * b
}
}
}
// In another crate, we can access `math::arithmetic::add`, but not `multiply`.
Qualified Visibility: pub(crate), pub(super), and pub(in path)
Rust allows granular control over visibility with qualified pub modifiers:
pub(crate): Exposes the item to the entire crate, but not to external crates.pub(super): Exposes the item to the parent module (but not to the entire crate or external crates).pub(in path): Exposes the item to a specific ancestor module (e.g.,pub(in crate::math)).
Example:
mod parent {
pub mod child {
pub(crate) fn crate_level() { // Accessible anywhere in the crate
println!("Crate-level access");
}
pub(super) fn parent_level() { // Accessible only in `parent`
println!("Parent-level access");
}
pub(in crate::parent) fn specific_ancestor() { // Same as `pub(super)` here
println!("Specific ancestor access");
}
}
fn use_child() {
child::parent_level(); // OK: `parent` is the parent of `child`
child::specific_ancestor(); // OK: `crate::parent` is the ancestor
}
}
fn main() {
parent::child::crate_level(); // OK: `pub(crate)`
// parent::child::parent_level(); // Error: `parent_level` is private to `parent`
}
Paths: Referencing Items in Modules
To use an item from another module, you need to specify its path. Paths can be absolute or relative.
Absolute Paths
An absolute path starts from the crate root and uses crate:: as the prefix. It is unambiguous and works anywhere in the crate.
Example:
// src/lib.rs
pub mod math {
pub mod arithmetic {
pub fn add(a: i32, b: i32) -> i32 { a + b }
}
}
// Absolute path to `add`: starts from the crate root
fn main() {
let sum = crate::math::arithmetic::add(2, 3);
println!("Sum: {}", sum); // Output: Sum: 5
}
Relative Paths
A relative path starts from the current module and uses self (current module), super (parent module), or the name of a sibling module as the prefix.
Example:
// src/lib.rs
pub mod math {
pub mod arithmetic {
pub fn add(a: i32, b: i32) -> i32 { a + b }
}
pub mod geometry {
use super::arithmetic; // Relative path to `arithmetic` (parent module is `math`)
pub fn area_with_offset(radius: f64, offset: i32) -> f64 {
let area = std::f64::consts::PI * radius * radius;
area + arithmetic::add(offset, 0) as f64 // Use `arithmetic::add`
}
}
}
Simplifying Paths with use Declarations
Repeating long paths (e.g., crate::math::arithmetic::add) can clutter code. The use declaration lets you import items into the current scope, simplifying access.
Importing Items
Use use to bring an item into scope:
// src/lib.rs
pub mod math {
pub mod arithmetic {
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn subtract(a: i32, b: i32) -> i32 { a - b }
}
}
// Import `add` and `subtract` into the current scope
use crate::math::arithmetic::{add, subtract};
fn main() {
let sum = add(2, 3); // No need for the full path
let diff = subtract(5, 1); // Simplified!
println!("Sum: {}, Diff: {}", sum, diff); // Output: Sum: 5, Diff: 4
}
Renaming with as
If two imported items have the same name, use as to rename one:
use crate::math::arithmetic::add;
use another_crate::utils::add as add_strings; // Rename to avoid conflict
fn main() {
let num_sum = add(2, 3);
let str_sum = add_strings("Hello, ", "world!");
}
Glob Imports with *
The * wildcard imports all public items from a module into the current scope. Use this sparingly, as it can pollute the namespace:
use crate::math::arithmetic::*; // Import all public items from `arithmetic`
fn main() {
let sum = add(2, 3);
let diff = subtract(5, 1);
}
Nested Modules: Building Hierarchies
Modules can be nested to create deep hierarchies, mirroring real-world relationships between components. For example, a game crate might have modules like game::player, game::enemy, and game::player::inventory.
Example project structure:
src/
├── lib.rs
└── game/
├── mod.rs # Root of `game` module
├── player/
│ ├── mod.rs # Root of `game::player`
│ └── inventory.rs # `game::player::inventory`
└── enemy.rs # `game::enemy`
In src/lib.rs:
pub mod game; // Expose the `game` module
In src/game/mod.rs:
pub mod player; // Expose `player`
pub mod enemy; // Expose `enemy`
In src/game/player/mod.rs:
pub mod inventory; // Expose `inventory`
pub struct Player {
name: String,
health: u32,
}
impl Player {
pub fn new(name: &str) -> Self {
Player { name: name.to_string(), health: 100 }
}
}
Crate Organization Best Practices
Separation of Concerns
Group related functionality into modules. For example, a utils module for helper functions, a network module for API calls, and a ui module for user interface code.
Minimizing Public API Surface
Expose only what’s necessary via pub. Keep implementation details private to reduce maintenance overhead and prevent breaking changes.
Using Workspaces for Large Projects
For very large projects, split code into multiple crates using a workspace. A workspace is a collection of crates that share a Cargo.lock and target directory, simplifying dependency management.
Example workspace structure:
my_workspace/
├── Cargo.toml # Workspace manifest
├── Cargo.lock
├── target/
├── my_library/ # Library crate
│ ├── Cargo.toml
│ └── src/lib.rs
└── my_binary/ # Binary crate (depends on `my_library`)
├── Cargo.toml
└── src/main.rs
In my_workspace/Cargo.toml:
[workspace]
members = [
"my_library",
"my_binary",
]
Conclusion
Rust’s module and crate system is a powerful tool for organizing code, enforcing encapsulation, and building scalable applications. By leveraging crates as compilation units and modules for internal organization, you can create code that is maintainable, reusable, and easy to navigate.
Key takeaways:
- Crates are the smallest compilation units (binary or library).
- Modules organize code within a crate and control access with
pub. - Paths and
usedeclarations simplify access to items across modules. - Visibility modifiers like
pub(crate)andpub(super)enable granular access control.
With these concepts, you’ll be well-equipped to structure Rust projects of any size.