We’ll start our module example by making a new project with Cargo, but instead
of creating a binary crate, we’ll make a library crate: a project that other
people can pull into their projects as a dependency. For example, the rand
crate discussed in Chapter 2 is a library crate that we used as a dependency in
the guessing game project.
We’ll create a skeleton of a library that provides some general networking
functionality; we’ll concentrate on the organization of the modules and
functions, but we won’t worry about what code goes in the function bodies.
We’ll call our library communicator
. To create a library, pass the --lib
option instead of --bin
:
$ cargo new communicator --lib
$ cd communicator
Notice that Cargo generated src/lib.rs instead of src/main.rs. Inside src/lib.rs we’ll find the following:
Filename: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
Cargo creates an example test to help us get our library started, rather than
the “Hello, world!” binary that we get when we use the --bin
option. We’ll
look at the #[]
and mod tests
syntax in the “Using super
to Access a
Parent Module” section later in this chapter, but for now, leave this code at
the bottom of src/lib.rs.
Because we don’t have a src/main.rs file, there’s nothing for Cargo to
execute with the cargo run
command. Therefore, we’ll use the cargo build
command to compile our library crate’s code.
We’ll look at different options for organizing your library’s code that will be suitable in a variety of situations, depending on the intent of the code.
For our communicator
networking library, we’ll first define a module named
network
that contains the definition of a function called connect
. Every
module definition in Rust starts with the mod
keyword. Add this code to the
beginning of the src/lib.rs file, above the test code:
Filename: src/lib.rs
mod network {
fn connect() {
}
}
After the mod
keyword, we put the name of the module, network
, and then a
block of code in curly brackets. Everything inside this block is inside the
namespace network
. In this case, we have a single function, connect
. If we
wanted to call this function from code outside the network
module, we
would need to specify the module and use the namespace syntax ::
like so:
network::connect()
.
We can also have multiple modules, side by side, in the same src/lib.rs file.
For example, to also have a client
module that has a function named
connect
, we can add it as shown in Listing 7-1:
Filename: src/lib.rs
mod network {
fn connect() {
}
}
mod client {
fn connect() {
}
}
Listing 7-1: The network
module and the client
module
defined side by side in src/lib.rs
Now we have a network::connect
function and a client::connect
function.
These can have completely different functionality, and the function names do
not conflict with each other because they’re in different modules.
In this case, because we’re building a library, the file that serves as the
entry point for building our library is src/lib.rs. However, in respect to
creating modules, there’s nothing special about src/lib.rs. We could also
create modules in src/main.rs for a binary crate in the same way as we’re
creating modules in src/lib.rs for the library crate. In fact, we can put
modules inside of modules, which can be useful as your modules grow to keep
related functionality organized together and separate functionality apart. The
way you choose to organize your code depends on how you think about the
relationship between the parts of your code. For instance, the client
code
and its connect
function might make more sense to users of our library if
they were inside the network
namespace instead, as in Listing 7-2:
Filename: src/lib.rs
mod network {
fn connect() {
}
mod client {
fn connect() {
}
}
}
Listing 7-2: Moving the client
module inside the
network
module
In your src/lib.rs file, replace the existing mod network
and mod client
definitions with the ones in Listing 7-2, which have the client
module as an
inner module of network
. The functions network::connect
and
network::client::connect
are both named connect
, but they don’t conflict
with each other because they’re in different namespaces.
In this way, modules form a hierarchy. The contents of src/lib.rs are at the topmost level, and the submodules are at lower levels. Here’s what the organization of our example in Listing 7-1 looks like when thought of as a hierarchy:
communicator
├── network
└── client
And here’s the hierarchy corresponding to the example in Listing 7-2:
communicator
└── network
└── client
The hierarchy shows that in Listing 7-2, client
is a child of the network
module rather than a sibling. More complicated projects can have many modules,
and they’ll need to be organized logically in order for you to keep track of
them. What “logically” means in your project is up to you and depends on how
you and your library’s users think about your project’s domain. Use the
techniques shown here to create side-by-side modules and nested modules in
whatever structure you would like.
Modules form a hierarchical structure, much like another structure in computing that you’re used to: filesystems! We can use Rust’s module system along with multiple files to split up Rust projects so not everything lives in src/lib.rs or src/main.rs. For this example, let’s start with the code in Listing 7-3:
Filename: src/lib.rs
mod client {
fn connect() {
}
}
mod network {
fn connect() {
}
mod server {
fn connect() {
}
}
}
Listing 7-3: Three modules, client
, network
, and
network::server
, all defined in src/lib.rs
The file src/lib.rs has this module hierarchy:
communicator
├── client
└── network
└── server
If these modules had many functions, and those functions were becoming lengthy,
it would be difficult to scroll through this file to find the code we wanted to
work with. Because the functions are nested inside one or more mod
blocks,
the lines of code inside the functions will start getting lengthy as well.
These would be good reasons to separate the client
, network
, and server
modules from src/lib.rs and place them into their own files.
First, let’s replace the client
module code with only the declaration of the
client
module so that src/lib.rs looks like code shown in Listing 7-4:
Filename: src/lib.rs
mod client;
mod network {
fn connect() {
}
mod server {
fn connect() {
}
}
}
Listing 7-4: Extracting the contents of the client
module but leaving the declaration in src/lib.rs
We’re still declaring the client
module here, but by replacing the block
with a semicolon, we’re telling Rust to look in another location for the code
defined within the scope of the client
module. In other words, the line mod client;
means this:
mod client {
// contents of client.rs
}
Now we need to create the external file with that module name. Create a
client.rs file in your src/ directory and open it. Then enter the
following, which is the connect
function in the client
module that we
removed in the previous step:
Filename: src/client.rs
fn connect() {
}
Note that we don’t need a mod
declaration in this file because we already
declared the client
module with mod
in src/lib.rs. This file just
provides the contents of the client
module. If we put a mod client
here,
we’d be giving the client
module its own submodule named client
!
Rust only knows to look in src/lib.rs by default. If we want to add more
files to our project, we need to tell Rust in src/lib.rs to look in other
files; this is why mod client
needs to be defined in src/lib.rs and can’t
be defined in src/client.rs.
Now the project should compile successfully, although you’ll get a few
warnings. Remember to use cargo build
instead of cargo run
because we have
a library crate rather than a binary crate:
$ cargo build
Compiling communicator v0.1.0 (file:///projects/communicator)
warning: function is never used: `connect`
--> src/client.rs:1:1
|
1 | / fn connect() {
2 | | }
| |_^
|
= note: #[warn(dead_code)] on by default
warning: function is never used: `connect`
--> src/lib.rs:4:5
|
4 | / fn connect() {
5 | | }
| |_____^
warning: function is never used: `connect`
--> src/lib.rs:8:9
|
8 | / fn connect() {
9 | | }
| |_________^
These warnings tell us that we have functions that are never used. Don’t worry
about these warnings for now; we’ll address them later in this chapter in the
“Controlling Visibility with pub
” section. The good news is that they’re just
warnings; our project built successfully!
Next, let’s extract the network
module into its own file using the same
pattern. In src/lib.rs, delete the body of the network
module and add a
semicolon to the declaration, like so:
Filename: src/lib.rs
mod client;
mod network;
Then create a new src/network.rs file and enter the following:
Filename: src/network.rs
fn connect() {
}
mod server {
fn connect() {
}
}
Notice that we still have a mod
declaration within this module file; this is
because we still want server
to be a submodule of network
.
Run cargo build
again. Success! We have one more module to extract: server
.
Because it’s a submodule—that is, a module within a module—our current tactic
of extracting a module into a file named after that module won’t work. We’ll
try anyway so you can see the error. First, change src/network.rs to have
mod server;
instead of the server
module’s contents:
Filename: src/network.rs
fn connect() {
}
mod server;
Then create a src/server.rs file and enter the contents of the server
module that we extracted:
Filename: src/server.rs
fn connect() {
}
When we try to cargo build
, we’ll get the error shown in Listing 7-5:
$ cargo build
Compiling communicator v0.1.0 (file:///projects/communicator)
error: cannot declare a new module at this location
--> src/network.rs:4:5
|
4 | mod server;
| ^^^^^^
|
note: maybe move this module `src/network.rs` to its own directory via `src/network/mod.rs`
--> src/network.rs:4:5
|
4 | mod server;
| ^^^^^^
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
--> src/network.rs:4:5
|
4 | mod server;
| ^^^^^^
Listing 7-5: Error when trying to extract the server
submodule into src/server.rs
The error says we cannot declare a new module at this location
and is
pointing to the mod server;
line in src/network.rs. So src/network.rs is
different than src/lib.rs somehow: keep reading to understand why.
The note in the middle of Listing 7-5 is actually very helpful because it points out something we haven’t yet talked about doing:
note: maybe move this module `network` to its own directory via
`network/mod.rs`
Instead of continuing to follow the same file-naming pattern we used previously, we can do what the note suggests:
- Make a new directory named network, the parent module’s name.
- Move the src/network.rs file into the new network directory and rename it src/network/mod.rs.
- Move the submodule file src/server.rs into the network directory.
Here are commands to carry out these steps:
$ mkdir src/network
$ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network
Now when we try to run cargo build
, compilation will work (we’ll still have
warnings though). Our module layout still looks exactly the same as it did when
we had all the code in src/lib.rs in Listing 7-3:
communicator
├── client
└── network
└── server
The corresponding file layout now looks like this:
└── src
├── client.rs
├── lib.rs
└── network
├── mod.rs
└── server.rs
So when we wanted to extract the network::server
module, why did we have to
also change the src/network.rs file to the src/network/mod.rs file and put
the code for network::server
in the network directory in
src/network/server.rs? Why couldn’t we just extract the network::server
module into src/server.rs? The reason is that Rust wouldn’t be able to
recognize that server
was supposed to be a submodule of network
if the
server.rs file was in the src directory. To clarify Rust’s behavior here,
let’s consider a different example with the following module hierarchy, where
all the definitions are in src/lib.rs:
communicator
├── client
└── network
└── client
In this example, we have three modules again: client
, network
, and
network::client
. Following the same steps we did earlier for extracting
modules into files, we would create src/client.rs for the client
module.
For the network
module, we would create src/network.rs. But we wouldn’t be
able to extract the network::client
module into a src/client.rs file
because that already exists for the top-level client
module! If we could put
the code for both the client
and network::client
modules in the
src/client.rs file, Rust wouldn’t have any way to know whether the code was
for client
or for network::client
.
Therefore, in order to extract a file for the network::client
submodule of
the network
module, we needed to create a directory for the network
module
instead of a src/network.rs file. The code that is in the network
module
then goes into the src/network/mod.rs file, and the submodule
network::client
can have its own src/network/client.rs file. Now the
top-level src/client.rs is unambiguously the code that belongs to the
client
module.
Let’s summarize the rules of modules with regard to files:
- If a module named
foo
has no submodules, you should put the declarations forfoo
in a file named foo.rs. - If a module named
foo
does have submodules, you should put the declarations forfoo
in a file named foo/mod.rs.
These rules apply recursively, so if a module named foo
has a submodule named
bar
and bar
does not have submodules, you should have the following files
in your src directory:
└── foo
├── bar.rs (contains the declarations in `foo::bar`)
└── mod.rs (contains the declarations in `foo`, including `mod bar`)
The modules should be declared in their parent module’s file using the mod
keyword.
Next, we’ll talk about the pub
keyword and get rid of those warnings!