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