Rust: Modules and Packages

Home · Blog

18 March 2023

In this article I’ll talk about how you group items into modules, crates, and packages, how you use items from other modules, and how you make items private or public to control where they can be used from. That may sound like a lot, but fortunately none of these things is very complicated in Rust.

Modules

A module groups zero or more items such as functions, structs, traits, and other modules (“item” is actually the technical term here) and lets you decide which of these are public and which are private. You can define a module with the keyword mod, like this:

pub mod numbers {
    fn zero() -> isize { 0 }
    pub fn one() -> isize { zero() + 1 }
}

Module numbers contains two items: functions zero and one. Because one is marked with pub, it can be called from other modules; zero can only be used from numbers itself and modules contained by numbers.

The module itself is also defined with pub here. If it wasn’t, then only its parent module could access it.

We could also have defined the module like this:

pub mod numbers;

and then have a file numbers.rs with this code:

fn zero() -> isize { 0 }
pub fn one() -> isize { zero() + 1 }

These two types of module definitions let you decide for each module whether it should go into a separate file or not. Unlike packages in Go, you can’t split one module across multiple files.

To access an item in another module you need to use the path to the item. The path tells Rust how to find the item, a bit like a path in the filesystem tells the OS where to find a file. It’s pretty straightforward:

Rust has the use declaration to bring names into the current scope. If you need foo::bar::baz in multiple places, you can put

use foo::bar::baz;

at the top of the file and then just use baz directly. Or you can just bring the module into scope with

use foo::bar;

and use bar::baz to refer to the item. The convention in the Rust community is to bring the module into scope for functions, but bring structs, enums and other items into scope directly.

If you want to import multiple items from the same module, you can do so in one declaration:

use std::io::{BufRead, BufReader};

You can bring all items from a module into scope with

use foo::*;

This is generally frowned upon, but there are two cases where it’s common. One is for unit tests (see Rust: Testing). The other is with a feature called re-exporting: when you bring a name into scope with pub use, you also make it available to other modules. This lets you split your code into modules internally but still have a single module as the public interface. You just need to define the internal modules with mod instead of pub mod and re-export the items you want to have public.

Packages and crates

So that’s modules; what about packages and crates? A package is basically what cargo new creates. It’s a directory with a Cargo.toml file where you can run cargo build, cargo test, and so on.

A library package, usually set up with cargo new --lib, contains exactly one crate. Its crate root is the file src/lib.rs, which means the library consists of the code in that file plus the files it refers to with the mod keyword.

A binary package, which is what cargo new creates by default, contains at least one binary crate and an optional library crate. Each binary is a file with a main function. Package with a single binary usually use the default location src/main.rs; if there’s more than one, each should be a file under src/bin. For example, if you have client and server binaries, your src directory might look like this:

$ tree src
src
├── bin
│   ├── client.rs
│   └── server.rs
└── lib.rs

and you can run the binaries with

cargo run server

and

cargo run client

Dependencies

If your code depends on an external library, you add it to your Cargo.toml, something like this:

[dependencies]
rand = "0.8.5"

This means the rand module -- that is, the module defined in src/lib.rs in the rand package -- is available in your code under the name rand.

If you have a binary crate and a library crate, you can use the library from the binary with your package’s name. If your package looks like this:

$ tree frobnicator
frobnicator
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── lib.rs
│   └── main.rs
└── target
    └── …

then a function foo in lib.rs can be called as frobnicator::foo from main.rs. Typically, binary crates in Rust are structured with almost all the logic in the library crate so the binary crates is just a bit of wrapper code.