17.3 Modules: Organizing Code Within a Crate
While packages and crates define compilation boundaries and dependency management, modules provide the mechanism for organizing code inside a single crate. Modules allow you to:
- Group related code: Place functions, structs, enums, traits, and constants related to a specific piece of functionality together.
- Control visibility (privacy): Define which items are accessible from outside the module.
- Create a hierarchical namespace: Avoid naming conflicts by nesting modules.
This system is Rust’s answer to namespace management and encapsulation, somewhat analogous to C++ namespaces or the C practice of using static
to limit symbol visibility to a single file, but with more explicit compiler enforcement and finer-grained control.
17.3.1 Module Basics and Visibility
Items defined within a module (or at the crate root) are private by default. Private items can only be accessed by code within the same module or in direct child modules.
To make an item accessible from outside its defining module, you must mark it with the pub
(public) keyword.
Code in one module refers to items in another module using paths, like module_name::item_name
or crate::module_name::item_name
. The use
keyword simplifies access by bringing items into the current scope.
17.3.2 Defining Modules: Inline vs. Files
Modules can be defined in two primary ways:
1. Inline Modules
Defined directly within a source file using the mod
keyword followed by the module name and curly braces {}
containing the module’s content.
// Crate root (e.g., main.rs or lib.rs) // Define an inline module named 'networking' mod networking { // This function is public *within* the 'networking' module // and accessible from outside if 'networking' itself is reachable. pub fn connect() { // Call a private helper function within the same module establish_connection(); println!("Connected!"); } // This function is private to the 'networking' module fn establish_connection() { println!("Establishing connection..."); // Implementation details... } } fn main() { // Call the public function using its full path networking::connect(); // This would fail compilation because establish_connection is private: // networking::establish_connection(); }
2. Modules in Separate Files
For better organization, especially with larger modules, their content is placed in separate files. You declare the module’s existence in its parent module (or the crate root) using mod module_name;
(without braces). The compiler then looks for the module’s content based on standard conventions:
- Convention 1 (Modern, Recommended): Look for
src/module_name.rs
. - Convention 2 (Older): Look for
src/module_name/mod.rs
.
Example (using src/networking.rs
):
Project Structure:
my_crate/
├── src/
│ ├── main.rs # Crate root
│ └── networking.rs # Contains the 'networking' module content
└── Cargo.toml
src/main.rs
:
// Declare the 'networking' module.
// The compiler looks for src/networking.rs or src/networking/mod.rs
mod networking; // Semicolon indicates content is in another file
fn main() {
networking::connect();
}
src/networking.rs
:
#![allow(unused)] fn main() { // Contents of the 'networking' module pub fn connect() { establish_connection(); println!("Connected!"); } fn establish_connection() { println!("Establishing connection..."); // Implementation details... } }
17.3.3 Submodules and File Structure
Modules can be nested to create hierarchies. If a module parent
contains a submodule child
, the file structure conventions extend naturally.
Modern Style (Recommended):
If src/parent.rs
contains pub mod child;
, the compiler looks for the child
module’s content in src/parent/child.rs
.
my_crate/
├── src/
│ ├── main.rs # Crate root, declares 'mod network;'
│ ├── network.rs # Declares 'pub mod client;'
│ └── network/ # Directory for submodules of 'network'
│ └── client.rs # Contains content of 'network::client' module
└── Cargo.toml
src/main.rs
:
mod network; // Looks for src/network.rs
fn main() {
// Assuming connect is pub in client, and client is pub in network
network::client::connect();
}
src/network.rs
:
// Declare the 'client' submodule. Make it public ('pub mod') if it needs
// to be accessible from outside the 'network' module (e.g., from main.rs).
// Looks for src/network/client.rs
pub mod client;
// Other items specific to the 'network' module could go here.
// E.g., pub(crate) struct SharedNetworkState { ... }
src/network/client.rs
:
#![allow(unused)] fn main() { // Contents of the 'network::client' module pub fn connect() { println!("Connecting via network client..."); } }
Older Style (Using mod.rs
):
If src/parent/mod.rs
contains pub mod child;
, the compiler looks for the child
module’s content in src/parent/child.rs
.
my_crate/
├── src/
│ ├── main.rs # Crate root, declares 'mod network;'
│ └── network/ # Directory for 'network' module
│ ├── mod.rs # Contains 'network' content, declares 'pub mod client;'
│ └── client.rs # Contains content of 'network::client' module
└── Cargo.toml
While both styles are supported, the non-mod.rs
style (network.rs
+ network/client.rs
) is generally preferred for new projects. It avoids having many files named mod.rs
, making navigation potentially easier, as the file name directly matches the module name. Consistency within a project is the most important aspect.
17.3.4 Controlling Visibility with pub
Rust’s visibility rules provide fine-grained control, defaulting to private for strong encapsulation.
private
(default, no keyword): Accessible only within the current module and its direct children modules (for items defined within the parent). Think of it like C’sstatic
for functions/variables within a file, but applied to all items and enforced hierarchically.pub
: Makes the item public. If an item ispub
, it’s accessible from anywhere its parent module is accessible.pub(crate)
: Visible anywhere within the same crate, but not outside the crate. Useful for internal helper functions or types shared across different modules of the crate but not part of its public API.pub(super)
: Visible only in the immediate parent module.pub(in path::to::module)
: Visible only within the specified module path (which must be an ancestor module). This is less common but offers precise scoping.
Visibility of Struct Fields and Enum Variants:
- Marking a
struct
orenum
aspub
makes the type itself public, but its contents follow their own rules:- Struct Fields: Fields are private by default, even if the struct itself is
pub
. You must explicitly mark fields withpub
(orpub(crate)
, etc.) if you want code outside the module to access or modify them directly. This encourages using methods for interaction (encapsulation). - Enum Variants: Variants of a
pub
enum are public by default. If the enum type is accessible, all its variants are also accessible.
- Struct Fields: Fields are private by default, even if the struct itself is
pub mod configuration { // Struct is public pub struct AppConfig { // Field is public pub server_address: String, // Field is private (only accessible within 'configuration' module) api_secret: String, // Field is crate-visible pub(crate) max_retries: u32, } impl AppConfig { // Public constructor (often named 'new') pub fn new(address: String, secret: String) -> Self { AppConfig { server_address: address, api_secret: secret, max_retries: 5, // Default internal value } } // Public method to access information derived from private field pub fn get_secret_info(&self) -> String { format!("Secret length: {}", self.api_secret.len()) } // Crate-visible method (could be used by other modules in this crate) pub(crate) fn set_max_retries(&mut self, retries: u32) { self.max_retries = retries; } } // Public enum pub enum LogLevel { Debug, // Variants are public because LogLevel is pub Info, Warning, Error, } } fn main() { let mut config = configuration::AppConfig::new( "127.0.0.1:8080".to_string(), "super-secret-key".to_string() ); // OK: server_address field is public println!("Server Address: {}", config.server_address); config.server_address = "192.168.1.100:9000".to_string(); // Modifiable // OK: max_retries is pub(crate), accessible within the same crate println!("Max Retries (initial): {}", config.max_retries); // config.set_max_retries(10); // We could call this if it were pub(crate) // Direct access would also work if main was in the same crate: // config.max_retries = 10; // println!("Max Retries (updated): {}", config.max_retries); // Error: api_secret field is private // println!("Secret: {}", config.api_secret); // config.api_secret = "new-secret".to_string(); // Cannot modify // OK: Access via public method println!("{}", config.get_secret_info()); // OK: Use public enum variant let level = configuration::LogLevel::Warning; }
17.3.5 Paths for Referring to Items
You use paths to refer to items (functions, types, modules) defined elsewhere.
- Absolute Paths: Start from the crate root using the literal keyword
crate::
or from an external crate’s name (e.g.,rand::
).crate::configuration::AppConfig::new(/* ... */); // Item in same crate std::collections::HashMap::new(); // Item in standard library rand::thread_rng(); // Item in external 'rand' crate
- Relative Paths: Start from the current module.
self::
: Refers to an item within the current module (rarely needed unless disambiguating).super::
: Refers to an item within the parent module. Can be chained (super::super::
) to go further up the hierarchy.
mod outer { pub fn outer_func() { println!("Outer function"); } pub mod inner { pub fn inner_func() { println!("Inner function calling sibling:"); self::sibling_func(); // Call function in same module ('inner') println!("Inner function calling parent:"); super::outer_func(); // Call function in parent module ('outer') } pub fn sibling_func() { println!("Sibling function"); } } } fn main() { outer::inner::inner_func(); }
Choosing between absolute (crate::
) and relative (super::
) paths is often a matter of style and context. crate::
is unambiguous but can be longer. super::
is concise for accessing parent items but depends on the current module’s location.
17.3.6 Importing Items with use
Constantly writing long paths like std::collections::HashMap
can be tedious. The use
keyword brings items into the current scope, allowing you to refer to them directly by their final name.
// Bring HashMap from the standard library's collections module into scope use std::collections::HashMap; // Bring the connect function from our hypothetical network::client module // Assume 'network' module is declared earlier or in another file mod network { pub mod client { pub fn connect() { /* ... */ } } } use crate::network::client::connect; fn main() { // Now we can use HashMap and connect directly let mut scores = HashMap::new(); scores.insert("Alice", 100); connect(); println!("{:?}", scores); }
Scope of use
: A use
declaration applies only to the scope it’s declared in (usually a module, but can also be a function or block). Siblings or parent modules are not affected; they need their own use
declarations if they wish to import the same items.
Common use
Idioms:
- Functions: Often idiomatic to import the function’s full path.
use crate::network::client::connect; connect(); // Call directly
- Structs, Enums, Traits: Usually idiomatic to import the item itself.
use std::collections::HashMap; let map = HashMap::new(); use std::fmt::Debug; #[derive(Debug)] // Use the imported trait struct Point { x: i32, y: i32 }
- Avoiding Name Conflicts: If importing two items with the same name, you can either import their parent modules and use full paths, or use
as
to rename one or both imports.use std::fmt::Result as FmtResult; // Rename std::fmt::Result use std::io::Result as IoResult; // Rename std::io::Result fn function_one() -> FmtResult { // ... implementation returning std::fmt::Result ... Ok(()) } fn function_two() -> IoResult<()> { // ... implementation returning std::io::Result ... Ok(()) } fn main() { function_one().unwrap(); function_two().unwrap(); }
Nested Paths in use
: Simplify importing multiple items from the same crate or module hierarchy.
// Instead of:
// use std::cmp::Ordering;
// use std::io;
// use std::io::Write;
// Use nested paths:
use std::{
cmp::Ordering,
io::{self, Write}, // Imports std::io, std::io::Write
};
// Or using 'self' for the parent module itself:
// use std::io::{self, Read, Write}; // Imports std::io, std::io::Read, std::io::Write
Glob Operator (*
): The use path::*;
syntax imports all public items from path
into the current scope. While convenient, this is generally discouraged in library code and application logic because it makes it hard to determine where names originated and increases the risk of name collisions. Its primary legitimate use is often within prelude modules (see Section 17.3.9) or sometimes in tests.
17.3.7 Re-exporting with pub use
Sometimes, an item is defined deep within a module structure (e.g., crate::internal::details::UsefulType
), but you want to expose it as part of your crate’s primary public API at a simpler path (e.g., crate::UsefulType
). The pub use
declaration allows you to re-export an item from another path, making it publicly available under the new path.
mod internal_logic { pub mod data_structures { pub struct ImportantData { pub value: i32 } pub fn process_data(data: &ImportantData) { println!("Processing data with value: {}", data.value); } } } // Re-export ImportantData and process_data at the crate root level. // Users of this crate can now access them directly via `crate::` pub use internal_logic::data_structures::{ImportantData, process_data}; // Optionally, re-export with a different name using 'as' // pub use internal_logic::data_structures::ImportantData as PublicData; fn main() { let data = ImportantData { value: 42 }; // Use the re-exported type process_data(&data); // Use the re-exported function }
pub use
is a powerful tool for designing clean, stable public APIs for libraries, hiding the internal module organization from users.
17.3.8 Overriding File Paths with #[path]
In rare situations, primarily when dealing with generated code or unconventional project layouts, the default module file path conventions (module_name.rs
or module_name/mod.rs
) might not apply. The #[path = "path/to/file.rs"]
attribute allows you to explicitly tell the compiler where to find the source file for a module declared with mod
.
// In src/main.rs or src/lib.rs
// Tell the compiler the 'config' module's code is in 'generated/configuration.rs'
#[path = "generated/configuration.rs"]
mod config;
fn main() {
// Assuming 'load' is a public function in the 'config' module
// config::load();
}
This attribute should be used sparingly as it deviates from standard Rust project structure.
17.3.9 The Prelude
Rust aims to keep the global namespace uncluttered. However, certain types, traits, and macros are so commonly used that requiring explicit use
statements for them everywhere would be overly verbose. Rust addresses this with the prelude.
Every Rust module implicitly has access to the items defined in the standard library prelude (std::prelude::v1
). This includes fundamental items like Option
, Result
, Vec
, String
, Box
, common traits like Clone
, Copy
, Debug
, Iterator
, Drop
, the vec!
macro, and more. Anything not in the prelude must be explicitly imported using use
.
Crates can also define their own preludes (often pub mod prelude { pub use ...; }
) containing the most commonly used items from that crate, allowing users to import them conveniently with a single use my_crate::prelude::*;
.