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.
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:
foo::bar is item bar inside module
foo.self is the current module.super is the parent module.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.
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. Packages
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
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 crate is just a bit of wrapper code.