A clear explanation of Rust’s module system

Rust’s module system can be confusing and frustrating for new users.

In this blog, I will explain the module system through real examples, so that you have a clear understanding of how the module system works and can immediately apply it to your project.

Since Rust’s module system is quite special, I hope readers can read this blog with an open mind and not learn by analogy between Rust’s module system and the working principles of modules in other programming languages.

Let’s use the following file structure to simulate a real project:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs

There are different ways that we can use our modules:

These three examples (each colored line and dependency represents a usage) are enough to explain how the Rust module system works.

Example 1

First example, let’s import config.rs in main.rs:

// main.rs
fn main() {
  println!("main");
}
// config.rs
fn print_config() {
  println!("config");
}

The first example, basically everyone makes the mistake because we have files like config.rs, health_routes.rs, etc., and we think these files are modules. And we can import them from other files. (analogous to other programming languages, such as python)

Here is the file tree as we see it from the file system perspective and the module tree as we see it from the compiler perspective:

Surprisingly, the compiler can only see the crate module, which is our main.rs file. This is because in Rust, we need to build the module tree explicitly – there is no implicit mapping between the file system tree and the module tree.

In Rust, we need to explicitly build the module tree. There is no implicit mapping between the module structure and the file structure!

In order to add a file to the module tree, we need to declare the file as a submodule via the mod keyword. The next confusing thing is that you would think that we declare a file as a module in the same file, but actually we need to declare a file as a module in a different file! Currently we only have one main.rs file in the module tree, let’s declare the config.rs file as a submodule in main.rs (submodule).

The mod keyword is used to declare a submodule

The syntax of the mod keyword is:

mod my_module;

If we use mod my_module to declare a submodule in the main.rs file, and the submodule name is my_module, the compiler will look for < in the same file directory as main.rs. code>my_module.rs or my_module/mod.rs:

// Structure 1
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── my_module.rs
  └─┬ my_module
    └── mod.rs

// Structure 2
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  └─┬ my_module
    └── mod.rs

Note: After Rust version 1.30, it is recommended to use .rs files (i.e. structure 1) of the same level and folder name to organize modules.

Back to our example. Because the main.rs and config.rs files are in the same directory, we declare a config module as follows:

// main.rs
 + mod config;

fn main() {

  • config::print_config();
    println!(“main”);
    }
// config.rs
fn print_config() {
  println!("config");
}

The code following the + sign in the file represents the newly added code, and the code following the – sign represents the deleted old code. It is the file change details seen through git diff.

We use the print_config function defined in config.rs through the :: syntax in the main function. Here’s what the module tree looks like:

img

We have successfully declared the config module! However, this is not enough for us to call the print_config method in config.rs. Because almost everything in Rust is private by default, we need to use the pub keyword to make the function public:

pub keyword makes things public

// main.rs
mod config;

fn main() {
config::print_config();
println!(“main”);
}

// config.rs
- fn print_config() {
 + pub fn print_config() {
  println!("config");
}

Adding pub in front of fn can make this function public.

Now, we can call the print_config function. We successfully called a function defined in a different file!

Example 2

Let us try to call the print_health_route function defined in routes/health_route.rs in main.rs:

// main.rs
mod config;

fn main() {
config::print_config();
println!(“main”);
}

// routes/health_route.rs
fn print_health_route() {
  println!("health_route");
}

As discussed earlier, we can only use the mod keyword for my_module.rs or my_module/mod.rs in the same directory.

So, in order to call the function defined in routes/health_route.rs in main.rs, we need to do the following:

  • Create a file called routes/mod.rs and declare the routes submodule in main.rs.
  • Declare the health_route submodule in routes/mod.rs and make it public
  • Make the functions in health_route.rs public
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
 + │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs
// main.rs
mod config;
 + mod routes;

fn main() {

  • routes::health_route::print_health_route();
    config::print_config();
    println!(“main”);
    }
// routes/mod.rs
 + pub mod health_route;
// routes/health_route.rs
- fn print_health_route() {
 + pub fn print_health_route() {
  println!("health_route");
}

The module tree will look like this:

img

Now we can call a function defined in a file in a directory.

Example 3

Let us try to call through a call chain like main.rs => routes/user_route.rs => modules/user_model.rs:

// main.rs
mod config;
mod routes;

fn main() {
routes::health_route::print_health_route();
config::print_config();
println!(“main”);
}

// routes/user_route.rs
fn print_user_route() {
  println!("user_route");
}
// models/user_model.rs
fn print_user_model() {
  println!("user_model");
}

We want to call print_user_route in the main function, and then call print_user_model in print_user_route.

Let’s make the same changes to the file as we did before – declare the submodule, make the function public and declare the mod.rs file:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
 + ├── mod.rs
    └── user_model.rs
// main.rs
mod config;
mod routes;
 + mod models;
fn main() {
routes::health_route::print_health_route();

routes::user_route::print_user_route();
config::print_config();
println!("main");
}
// routes/mod.rs
pub mod health_route;
 + pub mod user_route;
// routes/user_route.rs
- fn print_user_route() {
 + pub fn print_user_route() {
  println!("user_route");
}
// models/mod.rs
 + pub mod user_model;
// models/user_model.rs
- fn print_user_model() {
 + pub fn print_user_model() {
  println!("user_model");
}

The module tree will look like this:

Wait, we haven’t actually called the print_user_model from the print_user_route function yet! So far, we have only called functions defined in other modules in main.rs. How can we call functions defined in other modules in other files?

If we look at the module tree, the print_user_model function is under the crate::models::user_model path. So, to use a module outside of main.rs, we will consider using a path in the module tree that reaches the called module:

// routes/user_route.rs
pub fn print_user_route() {
 + crate::models::user_model::print_user_model();
  println!("user_route");
}

By using the absolute path of the function in the module tree, we can call a function defined in another file in a file other than main.rs.

super keyword

Access a function through the full path as above. If our file organization structure is relatively deep, the full path will be very long. We want to call print_health_route in print_user_route, and their module paths are respectively in crate::routes::health_route and crate:: routes::user_routepath.

We can do this by using absolute path names crate::routes::health_route::print_health_route(), but we can also use relative paths super::health_route::print_health_route() . Note that we use super to point to the parent scope.

In the module path, the super keyword points to the parent module

pub fn print_user_route() {
  crate::routes::health_route::print_health_route();
  // can also be called using
  super::health_route::print_health_route();
  println!(“user_route”);
}

use keyword

Using a full absolute path or a relative path like above would be verbose. To make names (function names or type names, etc.) shorter, we use the use keyword to bind the path to a new name or alias.

The use keyword is used to shorten the module path

pub fn print_user_route() {
  crate::models::user_model::print_user_model();
  println!("user_route");
}

The above code can be refactored into:

use crate::models::user_model::print_user_model;

pub fn print_user_route() {
    print_user_model();
    println!(“user_route”);
}

If instead of using the name print_user_model we can also give it an alias:

use crate::models::user_model::print_user_model as log_user_model;

pub fn print_user_route() {
    log_user_model();
    println!(“user_route”);
}

External modules

Dependencies added to Cargo.toml are accessible to all modules in the project. We don’t need to explicitly import or declare anything to use a dependency.

All modules in the project can access modules in external dependencies

For example, let’s say we added the rand crate to our project. We can use this directly in our code:

pub fn print_health_route() {
  let random_number: u8 = rand::random();
  println!("{}", random_number);
  println!("health_route");
}

We can also use use to make the path shorter:

use rand::random;

pub fn print_health_route() {
    let random_number: u8 = random();
    println!(“{}”, random_number);
    println!(“health_route”);
}

Summary

  • The module system is explicit – it has no one-to-one mapping to the file system
  • We declare the file as a module in the file’s parent module file rather than declaring itself as a module in the file itself
  • The mod keyword is used to declare submodules
  • We need to explicitly declare functions, structures, etc. as public so that they can be used in other modules. The pub keyword makes Items public.
  • The use keyword is used to shorten the module path and make it easier to use.
  • We don’t need to explicitly declare third-party modules (you can use them by introducing dependencies)

Original link: https://www.sheshbabu.com/posts/rust-module-system/

References:

https://blog.csdn.net/lzufeng/article/details/127720208