Skip to content

Latest commit

 

History

History
481 lines (381 loc) · 15.4 KB

ch07-01-mod-and-the-filesystem.md

File metadata and controls

481 lines (381 loc) · 15.4 KB

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

#[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

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.

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

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:

  1. Make a new directory named network, the parent module’s name.
  2. Move the src/network.rs file into the new network directory and rename it src/network/mod.rs.
  3. 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 for foo in a file named foo.rs.
  • If a module named foo does have submodules, you should put the declarations for foo 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!