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::*;`</pre>
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.