| Contents |

17 Crates, modules, and packages

In C and C++, managing large projects typically involves dividing code into multiple source files (.c, .cpp) and using header files (.h, .hpp) to declare shared interfaces (functions, types, macros). While this approach is fundamental, it presents challenges: potential global namespace collisions, complex build system configurations (e.g., Makefiles, CMake) needed to track dependencies, and the exposure of internal implementation details through header files required for compilation.

Rust addresses code organization and dependency management with a more explicit and hierarchical system built on three core concepts: packages, crates, and modules.

This chapter delves into Rust’s module system. We’ll explore how code is structured within crates using modules, how packages group crates, how workspaces manage multiple related packages, and how Cargo orchestrates the entire process. We assume basic familiarity with Cargo from previous chapters; a more detailed examination of Cargo’s features will follow later.


17.1 Packages: Bundling Crates with Cargo

A package is the fundamental unit Cargo works with. It represents a Rust project, containing the source code, configuration, dependencies, and metadata necessary to build one or more crates. Every package is defined by its Cargo.toml manifest file located at the package root.

17.1.1 Creating a New Package

Cargo provides convenient commands to initialize a new package structure:

# Create a new package for a binary executable
cargo new my_executable_project

# Create a new package for a library
cargo new my_library_project --lib

For a binary package my_executable_project, Cargo generates:

my_executable_project/
├── Cargo.toml      # Package manifest
└── src/
    └── main.rs     # Crate root for the primary binary crate

For a library package my_library_project, it generates:

my_library_project/
├── Cargo.toml      # Package manifest
└── src/
    └── lib.rs      # Crate root for the library crate

17.1.2 Anatomy of a Package

A typical Rust package consists of:

17.1.3 Workspaces: Managing Multiple Packages

For larger projects involving several interdependent packages, Cargo offers workspaces. A workspace allows multiple packages to share a single Cargo.lock file (ensuring consistent dependency versions across the workspace) and a common target/ build directory (potentially speeding up compilation by sharing compiled dependencies).

A workspace is defined by a root Cargo.toml that designates member packages. The member packages still have their own individual Cargo.toml files for package-specific metadata and dependencies.

my_workspace/
├── Cargo.toml         # Workspace manifest (defines members)
├── package_a/         # Member package (e.g., a library)
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── package_b/         # Member package (e.g., a binary depending on package_a)
    ├── Cargo.toml
    └── src/
        └── main.rs

The root Cargo.toml (in my_workspace/) specifies the members:

[workspace]
members = [
    "package_a",
    "package_b",
    # Can also use glob patterns like "crates/*"
]

# Optional: Define shared profile settings for all members
# [profile.release]
# opt-level = 3

# Note: Dependencies defined here are NOT automatically inherited by members.
# Each member package lists its own dependencies in its own Cargo.toml.
# However, a [workspace.dependencies] table can define shared versions
# that members can inherit explicitly.

Running cargo build, cargo test, etc., from the workspace root (my_workspace/) will operate on all member packages.

17.1.4 Multiple Binaries within a Package

A single package can produce multiple executables.

my_package/
├── Cargo.toml
└── src/
    ├── main.rs         # Compiles to 'my_package' executable
    └── bin/
        ├── cli_tool.rs # Compiles to 'cli_tool' executable
        └── server.rs   # Compiles to 'server' executable

This structure is useful for packaging a collection of related tools together. Both src/main.rs and the files in src/bin/ can share code from src/lib.rs if it exists in the same package.

17.1.5 Distinguishing Packages and Crates

It’s crucial to understand the distinction:

Specifically, a single package can contain:

In simple projects with only src/main.rs or src/lib.rs, the package effectively contains just one crate. The distinction becomes important in larger projects, libraries with associated binaries, or workspaces where Cargo orchestrates the building of packages which, in turn, produce compiled crates.


17.2 Crates: Rust’s Compilation Units

A crate is the fundamental unit passed to the Rust compiler (rustc). Each crate is compiled independently, producing a single artifact (library or executable). This separation is key to Rust’s modularity, enabling separate compilation, effective optimization boundaries, and clear dependency management. Conceptually, a Rust crate is analogous to a single shared library (.so, .dylib), static library (.a, .lib), or executable produced by a C/C++ build process.

17.2.1 Binary vs. Library Crates

Cargo identifies crate roots by convention within the src/ directory:

17.2.2 The Crate Root and Module Tree

The crate root file (lib.rs for a library crate, src/main.rs for the primary binary crate) serves as the entry point for the compiler within that crate. The code contained directly within this file forms the top-level scope of the crate’s module tree. Thus, for any item defined directly in a crate root file (e.g., lib.rs or main.rs), the crate root file acts as its defining module. All other modules defined within the crate become descendants of this root. The special path crate:: always refers to this root of the current crate’s module tree, allowing unambiguous access to items defined at the top level of the crate or in its modules.

For packages with additional binary crates in src/bin/, each file in that directory (src/bin/name.rs) also serves as the root for a separate binary crate. The crate:: path used within src/bin/name.rs refers to the root of the name binary crate.

17.2.3 Using External Crates (Dependencies)

To leverage code from external libraries (crates), you first declare them as dependencies in your package’s Cargo.toml:

[dependencies]
# Dependency from crates.io (version "0.8.x", compatible with 0.8)
rand = "0.8"

# Dependency with specific features enabled
serde = { version = "1.0", features = ["derive"] }

# Dependency from a local path (e.g., within a workspace)
# my_local_lib = { path = "../my_local_lib" }

# Dependency from a Git repository
# some_crate = { git = "https://github.com/user/repo.git", branch = "main" }

When you build your package, Cargo automatically downloads (if necessary), compiles, and links these dependency crates. Within your Rust code, you can then access items (functions, types, etc.) defined in a dependency crate using the use keyword to bring them into scope:

// Import the `Rng` trait from the `rand` crate
use rand::Rng;

fn main() {
    // `rand::thread_rng()` returns a thread-local random number generator
    let mut rng = rand::thread_rng();
    // `gen_range` is a method provided by the `Rng` trait
    let n: u32 = rng.gen_range(1..101); // Generates a number between 1 and 100
    println!("Random number: {}", n);
}

Note: The Rust Standard Library (std) is implicitly linked and available. You don’t need to declare std in Cargo.toml. You access its components using paths like std::collections::HashMap or by bringing them into scope with use std::collections::HashMap;.

17.2.4 Historical Note: extern crate

In older Rust editions (specifically, Rust 2015), it was necessary to explicitly declare your intent to link against and use an external crate within your source code using extern crate crate_name; at the crate root.

// Rust 2015 style - generally not needed in Rust 2018+
extern crate rand;

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u32 = rng.gen_range(1..101);
    println!("Random number: {}", n);
}

Since the Rust 2018 edition, Cargo automatically handles this based on the [dependencies] section in Cargo.toml. The extern crate declaration is now implicit and generally omitted, except for a few specific advanced use cases (like renaming crates globally or using macros from crates without importing other items). For C programmers, this change makes dependency usage feel slightly more like including a header that makes library functions available, but with the crucial difference that Cargo manages the actual linking based on Cargo.toml.


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:

  1. Group related code: Place functions, structs, enums, traits, and constants related to a specific piece of functionality together.
  2. Control visibility (privacy): Define which items are accessible from outside the module.
  3. 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 are only accessible from within their defining module and its descendant modules. This hierarchical rule also implies that code in a module cannot access private items defined in its sibling modules (modules declared within the same parent); visibility for private items flows down the tree, not sideways.

To make an item accessible from outside its defining module, you must mark it with the pub (public) keyword. An item marked pub is accessible from its parent module and any scope that can reach the parent module. For example, if you declare mod my_module { pub fn my_function() {} } at the crate root (main.rs or lib.rs), code directly in the crate root can call my_module::my_function(), even though my_module itself isn’t marked pub. Marking the module pub mod my_module { ... } is necessary only if you want code outside the parent (e.g., in another crate if the parent is the crate root) to be able to traverse my_module’s path to access its public items.

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:

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:

// 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:

// 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.

Path Visibility Rule: For any item to be accessible, every module in its path, from the crate root down to the item itself, must be visible from the point of access. Even if an item is pub or pub(crate), it cannot be accessed if one of its parent modules in the path is private relative to the accessing code’s scope. You must be able to traverse the entire path.

Visibility of Struct Fields and Enum Variants:

pub mod configuration {
    // Struct is public - the type AppConfig can be named outside this module.
    pub struct AppConfig {
        // Field is public - code outside can access config.server_address
        pub server_address: String,
        // Field is private (default) - only code in 'configuration' module
        // and its descendants can access config.api_secret directly.
        api_secret: String,
        // Field is private (default) - only code within the 'configuration' module
        // and its descendants can access config.max_retries directly.
        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, // Can access private field within the module
                max_retries: 5, // Can access private field within the module
            }
        }

        // Public method to access information derived from private field
        pub fn get_secret_info(&self) -> String {
            // Can access private field within the module
            format!("Secret length: {}", self.api_secret.len())
        }

        // Crate-visible method (could be used by other modules in this crate)
        // Note: This method provides controlled modification of the `max_retries`
        // field from elsewhere within the crate, even though the field itself is
        // private to this module.
        pub(crate) fn set_max_retries(&mut self, retries: u32) {
            self.max_retries = retries;
        }
        
        // Example of a pub(crate) getter for the now private max_retries field
        pub(crate) fn get_max_retries(&self) -> u32 {
            self.max_retries
        }
    }

    // Public enum
    pub enum LogLevel {
        Debug, // Variants are public because LogLevel is pub
        Info,
        Warning,
        Error,
    }
}

fn main() {
    // OK: AppConfig is pub, so we can use the type and its public constructor
    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

    // Error: max_retries field is private within the 'configuration' module.
    // Direct access from outside the module (like from main.rs) is not allowed.
    // println!("Max Retries (initial): {}", config.max_retries); // Compile error
    // config.max_retries = 10; // Compile error

    // OK: Use the pub(crate) method to modify the private max_retries field
    config.set_max_retries(10);
    // OK: Use the pub(crate) method to access the private max_retries field
    println!("Max Retries (updated via method): {}", config.get_max_retries());

    // Error: api_secret field is private within the 'configuration' module
    // println!("Secret: {}", config.api_secret); // Cannot access
    // config.api_secret = "new-secret".to_string(); // Cannot modify

    // OK: Access information derived from private field via a public method
    println!("{}", config.get_secret_info());

    // OK: Use public enum variant (since LogLevel is pub)
    let level = configuration::LogLevel::Warning;
    match level {
        configuration::LogLevel::Warning => println!("Log level is Warning"),
        _ => {},
    }
}

The example above illustrates an important concept: the visibility of a struct (pub struct AppConfig) determines whether the type can be used outside its module. The visibility of its fields (pub, pub(crate), or private) determines whether code outside the module can directly access those fields using dot notation (config.field_name). This allows creating public types that carefully control access to their internal data, a cornerstone of encapsulation.

17.3.5 Paths for Referring to Items

You use paths to refer to items (functions, types, modules) defined elsewhere.

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;

fn main() {
    // Now we can use HashMap directly
    let mut scores = HashMap::new();
    scores.insert("Alice", 100);
    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:

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::*;.


17.4 Best Practices and Considerations

Effectively using packages, crates, and modules is key to building maintainable Rust applications.

17.4.1 Structuring Larger Projects

  1. Group by Feature/Responsibility: Organize modules around distinct features or areas of responsibility rather than arbitrary categories like “utils” or “helpers”, which tend to become dumping grounds for unrelated code.
  2. Meaningful Names: Choose clear, descriptive names for packages, crates, and modules that indicate their purpose.
  3. Control Visibility Aggressively: Default to private. Use pub only for items that constitute the intended public API of a module or crate. Use pub(crate) extensively for internal implementation details shared across modules within the same crate. This enforces encapsulation, reduces unintended coupling, and makes refactoring safer. This contrasts sharply with C/C++, where visibility control is often less granular or relies heavily on convention (like _ prefixes).
  4. Maintain a Reasonable Module Depth: Excessively nested modules (a::b::c::d::e::f::Item) can make paths unwieldy and code hard to navigate. Consider flattening the hierarchy or using pub use to re-export key items at more accessible levels (designing a facade).
  5. Be Consistent with File Structure: Choose one convention for module files (module.rs + module/child.rs or module/mod.rs + module/child.rs) and apply it consistently throughout the project. The former is generally preferred in modern Rust.
  6. Document Public APIs: Use documentation comments (/// for items, //! for modules/crates) to explain the purpose, usage, and any invariants of all pub items. Tools like cargo doc --open generate browseable HTML documentation from these comments.

17.4.2 Conditional Compilation (#[cfg])

Rust’s module system works seamlessly with conditional compilation attributes (#[cfg(...)] and #[cfg_attr(...)]). You can conditionally include or exclude entire modules or specific items within modules based on the target operating system, architecture, enabled Cargo features, or custom build script flags.

// Example: Platform-specific modules
#[cfg(target_os = "windows")]
mod windows_impl {
    pub fn setup() { /* Windows-specific setup */ }
}

#[cfg(target_os = "linux")]
mod linux_impl {
    pub fn setup() { /* Linux-specific setup */ }
}

// Common function calling the platform-specific version
pub fn platform_specific_setup() {
    #[cfg(target_os = "windows")]
    windows_impl::setup();

    #[cfg(target_os = "linux")]
    linux_impl::setup();

    #[cfg(not(any(target_os = "windows", target_os = "linux")))]
    {
        // Fallback or stub for other OSes
        println!("Platform setup not implemented for this OS.");
    }
}

// Example: Feature-gated module
#[cfg(feature = "experimental_feature")]
pub mod experimental {
    pub fn activate() { /* ... */ }
}

This is essential for writing portable code or implementing optional functionality without cluttering the main codebase.

17.4.3 Avoiding Cyclic Dependencies

The Rust compiler strictly enforces that dependencies must form a Directed Acyclic Graph (DAG). This applies both to dependencies between modules within a crate and dependencies between crates.

This restriction prevents many complex build and linking problems common in C/C++ projects where implicit or explicit cyclic dependencies between compilation units or libraries can arise, often requiring careful ordering in build systems or leading to fragile designs.

If you find yourself seemingly needing a cyclic dependency in Rust, it’s a signal that your code structure needs refactoring:

17.4.4 When to Split into Separate Crates

Deciding whether to separate functionality into different modules within a single crate or into entirely separate crates (perhaps within a workspace) involves trade-offs:

Reasons to prefer separate crates:

Reasons to prefer modules within a single crate:

Generally, start with modules within a single crate. Split into separate crates when the code becomes truly reusable, needs independent release cycles, benefits significantly from stricter encapsulation, or when the project structure grows complex enough that logical separation into distinct buildable units (crates) improves clarity and management (often using workspaces).


17.5 Summary

Rust employs a structured, hierarchical system for code organization and dependency management, offering significant advantages over traditional C/C++ approaches, particularly regarding namespace control, visibility, and build consistency.

This layered system promotes modularity, explicit dependencies, and clear API boundaries. By enforcing strict rules, such as the prevention of cyclic dependencies and default privacy, Rust encourages designs that are often more robust and maintainable than what might naturally arise in C or C++. While adapting from the .c/.h file model requires understanding these new concepts, the benefits in terms of project scalability, code clarity, and reduced build complexity typically become evident quickly.



| Contents |