Intro

As projects grow, you’ll need to refactor them into separate files and modules, sometimes into separate packages, for organization, maintainability, and reusability.

In this chapter we’ll be going over some of the ways we structure Rust projects.

  • Packages: A Cargo feature that lets you build, test, and share crates
  • Crates: A tree of modules that produces a library or executable
  • Modules and use: Let you control the organization, scope, and privacy of paths
  • Paths: A way of naming an item, such as a struct, function, or module

Packages and Crates

Crates

This is the most atomic amount of Rust code that the compiler would consider at any time. Even running a single .rs file using rustc instead of Cargo, the compiler would treat that file as a crate.

Crates can be a binary crate or a library crate.

Binary Crates

These a re programs that can be compiled and run and runnable, like a command line program or a server. They must have a main function to act as the entry way when the program runs. // This is what you know thus far, it's the typical Rust program.

Library Crates

Don’t have main functions because they don’t compile to run. Their functionality is meant to be shared (and used) by other programs (crates) that do run.

// In online discourse, when the community says "crate", most of the time they mean library crates.

Packages

A package is a bundle of one or more crates that provide a set of functionality. Every package contains a Cargo.toml file that describes how to build those crates.

For example, Cargo itself is actually a package that has a binary for the CLI tool used to build projects, it also contains the library crate the binary crate requires.

Package Composition

Packages can contain as many binary crates as we’d like but only one library crate at a time.

Cargo’s Convention

Whenever we make a project using Cargo, it relies on the src/main.rs being there to be the entry point for execution. There’s nothing of this mentioned in Cargo.toml. Likewise, it knows that if the package’s directory has a folder src/lib.rs then that package contains a library crate with the same name as the package and that src/lib.rs is the library crate’s root.

Then Cargo will pass the crate root files to rustc to build the binary or library.

“If a package contains src/main.rs and src/lib.rs, it has two crates: a binary and a library, both with the same name as the package. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.”

Defining Modules, Privacy, and Scope

There are 3 important keywords to know.

  1. use let’s you bring a path into scope, for example importing another module’s functions into the one you’re working in.
  2. pub sets members of a module as public to other modules. Everything’s private by default.
  3. as which allows assigning aliases to modules. Used with use keyword.

Modules

Crate’s root: when compiling a crate, the compiler will search for the root, as mentioned before it’s either src/main.rs or src/lib.rs to find the code it needs to compile.

Declaring Modules

Modules can be declared in the root files using the mod mod_name keyword. for example: mod Car; Compiler will search for the module’s code in the following locations:

  • Inline, within curly brackets that replace the semicolon following mod garden
mod Car{
	// Car's code and functions...
}
  • In the file src/car.rs
  • In the file src/car/mod.rs

Sub-Modules

Any module can contain any number of sub-modules. For example, our car can have a mod engine; And just like with modules, the compiler will search in:

  • Inline, directly following mod engine {};
  • In the file src/car/engine.rs
  • In the file src/car/engine/mod.rs

Paths to module code

Once a mod is declared and if the privacy rules allow, we can simply access the code using the :: operator to access the namespace and use its functions.

Privacy

A Module’s code is private from its parent modules by default unless declared it’s declared using pub mod instead of mod.

The same goes for members of a public module, to make functions public their declarations need to be preceded using pub as well.

Using Modules

To use the modules without repetitively using their namespaces, the use keywords allows us to import them once int a module.

All together

Created a binary crate named garage that illustrates these rules. The crate’s directory, also named “garage”, contains these files and directories:

garage
├── Cargo.lock
├── Cargo.toml
└── src
    ├── car
    │   └── engine.rs
    ├── car.rs
    └── main.rs

The crate root file in this case is src/main.rs Example contents:

use crate::car::engine::V8; 
 
pub mod car; // line tells the compiler to include the code
 
fn main() {
    let engine = V8 {};
    println!("I'm revving my {engine:?}!");
}

Grouping Code into Modules

The main reason we break things down in programming, is organization. Organized code is readable, modular, reusable, and maintainable. It also allows us to control privacy, exposing only the APIs we need, i.e. Encapsulation.

Creating New Library

To create a new library run cargo new <lib_name> --lib. Then navigate to src/lib.rs to define the modules and their function signatures; this serves as the API.

In the restaurant example they’re demonstrating, this would be the exposed front-house:

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
 
        fn seat_at_table() {}
    }
 
    mod serving {
        fn take_order() {}
 
        fn serve_order() {}
 
        fn take_payment() {}
    }
}

Modules let us group related (functionally or semantically) items together.

A Module can define more than just functions, it can define structs, enums, constants, and traits.

The src/lib.rs and src/main.rs modules are called crate roots because they create a module named crate that lives in the root of the crate’s module structure. If this were to be represented using a tree it’d look like this:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Notice how some modules are nested within others, whereas others are siblings sharing the same parent module.

If module A is contained inside module B then module A is the child of module B and that module B is the parent of module A. The entire module tree is under the implicitly created, module crate, which is the parent of all the modules.

Referring to Items in the Module Tree

To give Rust the path to module members we need there’s two ways.

  • An absolute path is the full path starting from a crate root.
    • For code from an external crate, the absolute path begins with the crate name, and for code from the current crate, it starts with the literal crate.
  • A relative path starts from the current module and uses self, super, or an identifier in the current module. Both ways paths are followed by one or more identifiers separated by double colons by ::.

This portion of the chapter is dense but contains some best practices and how the Rust community does things, I’ll simply reference it here.

Public Structs and Enums

Using pub only makes a struct or enum public, not its fields. To make them public we can choose which to expose, also using the pub keyword.

The same goes for associated functions define inside an enum or struct.

“Enums aren’t very useful unless their variants are public; it would be annoying to have to annotate all enum variants with pub in every case, so the default for enum variants is to be public.”

The use keyword and Scopes

As mentioned before, instead of writing the paths over and over again we can bring them into scope with the use keyword. // often at the top of a file/module. It’s similar to making a symbolic link in a file system.

Idiomatic use

The convention is to bring in the parent of the item you want to use. Then using the :: operator to tap into it when you need it, this reduces the amount of use statements for each child of a module… For example: use crate::front_of_house::hosting and then calling hosting::add_to_waitlist in eat_at_restaurant.

However, with structs, enums, and other items, the convention is to specify the full path. E.g. use std::collections::HashMap; instead of use std::collections; // In think this is to prevent loading items you have no use for, but have no proof...

Aliases with as keyword

This is great for when you need to import items with the same name but from different paths or modules. By assigning aliases you avoid confusing the two and leading to all sorts of havoc. For example:

use std::fmt::Result;
use std::io::Result as IoResult;
 
fn function1() -> Result {
    // --snip--
}
 
fn function2() -> IoResult<()> {
    // --snip--
}

Re-Exports using pub use

Names brought into scope are private. However, sometimes you may need to refer to that name outside, and that’s where pub + use come in. This is known as Re-Exporting.

Example:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
 
pub use crate::front_of_house::hosting;
 
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Re-exporting is handy when the internal structure of our project is different from how programmers calling and using the project’s code would think of the domain,thus improving the library’s organization.

The Glob Operator

To bring ALL the items in a path into scope, specify it with use and then use * operator at the end of the path.

use std::collections::*;

This will bring everything into scope from the Collections module.

However this makes it hard to tell what’s actually in scope and where a name is defined in the program, and can cause issues if things are moved around…

The Glob operator is used in testing modules to bring everything in scope to be tested.

Breaking Down Modules into Different Files

Although you can define modules in a single file, and there are cases when that’s best. Most times you’re likely better off to separate your modules into their own files. This makes it easier to navigate the code base.

In the module’s file declare it as a module using pub mod <name>; and expose items using pub as appropriate.

Summary

Alternate File Paths

So Rust supports two styles of file paths for imports. For a module named front_of_house declared in the crate root, the compiler will look for the module’s code in:

  • src/front_of_house.rs the current style.
  • src/front_of_house/mod.rs older style that’s still supported.

For a module named hosting that is a submodule of front_of_house, the compiler will look for the module’s code in:

  • src/front_of_house/hosting.rs the current style.
  • src/front_of_house/hosting/mod.rs older style that’s still supported.

Don't use both styles for the same module, you’ll get a compiler error.

Using a mix of both styles for different modules in the same project is allowed, but might confuse other developers using your project…

I don’t like the the style that uses files named mod.rs because your project becomes cluttered with many mod.rs files.


Summary

  • We can break down our code into modules, and organize them into separate files.
  • Packages are made of crates, some are binary crates and others are library crates.
  • A Package must be at least one crate, and may only have one library crate at the time.
  • We can bring code into scope with the use keyword.
  • Module code is private by default unless made public using pub keyword, and their members must also be made public to be used.