Modules, crates and packages

Rust · Practical · 14 min

Every project beyond one file needs modules, and every real project depends on crates from crates.io.

A package is what cargo new creates: a Cargo.toml plus one or more crates. A crate is a compilation unit, either a binary (rooted at src/main.rs) or a library (rooted at src/lib.rs). A package can have both; the binary then uses the library by its package name.

Inside a crate, code is organised into a tree of modules. mod parser; in main.rs or lib.rs tells the compiler to load src/parser.rs (or src/parser/mod.rs). Submodules of parser live in src/parser/. You can also write a module inline: mod tests { ... }.

Everything is private by default to its module (and that module's children). Mark items pub to expose them; pub(crate) exposes within the crate only. For structs, each field needs its own pub; for enums, making the enum pub makes all variants public.

Paths: crate::parser::parse is absolute from the crate root, super:: goes up one level, self:: is the current module. use brings names into scope; idiomatic style imports the parent module for functions (use std::fs; then fs::read) and the item itself for types (use std::collections::HashMap;). pub use re-exports, letting you present a flatter public API than your file layout.

Dependencies go in Cargo.toml under [dependencies], e.g. serde = { version = "1", features = ["derive"] }, or simply run cargo add serde --features derive. Version requirements are semver ranges: "1.4" means at least 1.4.0 and below 2.0.0. Cargo.lock records exact versions; commit it for applications. [dev-dependencies] are only for tests and examples. Large projects group crates in a workspace sharing one lock file and target/ directory.

Example

// src/lib.rs
pub mod shapes {
    #[derive(Debug)]
    pub struct Circle {
        pub radius: f64,
        id: u32, // private: only `shapes` and its child modules can touch it
    }

    impl Circle {
        pub fn new(radius: f64) -> Self { Self { radius, id: next_id() } }
    }

    fn next_id() -> u32 { 1 } // private helper

    pub mod util {
        pub fn describe(c: &super::Circle) -> String {
            format!("circle #{} r={}", c.id, c.radius) // child module sees parent's private items
        }
    }
}

pub use shapes::Circle; // re-export: callers can write my_crate::Circle

// src/main.rs:  use my_crate::{Circle, shapes::util::describe};

Remember

  • A package holds crates; a crate is a tree of modules rooted at main.rs or lib.rs
  • Items and struct fields are private unless marked pub
  • cargo add and [dependencies] pull crates from crates.io using semver ranges

Watch out for

  • Writing mod foo; in every file that uses foo; declare a module once, then use it elsewhere.

Check yourself

  1. A struct is `pub` but its fields are not. What can code in another module do?
  2. What does the dependency requirement `rand = "0.8.5"` allow?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html