mod
and the Filesystem
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
# #![allow(unused_variables)]
#fn main() {
#[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.
Module Definitions
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
# #![allow(unused_variables)]
#fn main() {
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
# #![allow(unused_variables)]
#fn main() {
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
# #![allow(unused_variables)]
#fn main() {
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.
Moving Modules to Other Files
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
# #![allow(unused_variables)]
#fn main() {
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
# #![allow(unused_variables)]
#fn main() {
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
# #![allow(unused_variables)]
#fn main() {
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
# #![allow(unused_variables)]
#fn main() {
fn connect() {
}
#}
When we try to run 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.
Rules of Module Filesystems
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!