When beginning with Rust and having the need to expand to other files in subfolders of the project, one can stumble over the module concept of Rust very fast.
Whatever technology or programming language we use, we tend to structure our ideas and concepts in directories and files, in order to maintain a valuable overview as the project accelerates and expands, to encapsulate knowledge, and to create strong coherence.
Creating these folders in a Rust project, it strikes home fast: Things are very different here. A C-based language programmer tends to import files to get the respective types or namespaces within one’s scope. Playing around with some context help of the IDE or searching the internet, we learn that Rust thinks in modules, not files.
Rust works with modules, as part of a module tree. There is no such thing as file imports.
A Module in Rust
Important concepts in the Rust Module System¹ are packages, crates, modules, and paths. This article focuses on modules² and paths, when defined in multiple files and how to bring the split parts together.
Creating a base project
Creating a new project from a template in one’s favorite IDE (or terminal editor) is usually the first step into a new project, and the Rust project will probably look similar to this:
The interesting part, with respect to modules, plays its part in the
src subfolder of our project. Looking into the file structure beneath
src, we see:
The contents are fairly simple. It’s just the
main.rs file, the entry point of our project, containing the
main function, usually being created like this:
src/main.rs (in case of an executable) or
src/lib.rs (in case of a library) files are called crate roots.
lib.rs act as the entry point into a package for the Rust compiler rustc.
We can, of course, build and run the project, as should be expected of a raw template. We can set up our base camp right here and talk about how to climb that mountain.
We want to build a project that consists of two modules being imported from
src/main.rs. These modules will expose public functionality as well as define module-only parts. The project will also demonstrate how modules can use functionality from other modules.
There are two modules: a power plant and a power-consuming tower.
Let’s think of our crate root (
src/main.rs) as a street. We want to construct two buildings next to each other: a tower building and an auxiliary power building. These two buildings will be our two modules. Our construction site (project) has dependencies between the street and its buildings, the power plant, generating power, and the tower building consuming it.
Adding files and folders to the project
When defining a module that is to be split into folders and files, we create a directory, and, on level with the crate root (
lib.rs), another file. Both carry the module name as directory- and filename. A structure for the module
example_module would be:
In our case, creating two buildings, or modules, we have to create two directories and two files. This results in a new folder structure in our project:
Of course, we have to also write some code to glue together the mere file system items. A module, represented by a directory, can consist of various files. Each of these files might define new types or symbols in Rust. The only missing part is how to bring the parts together. How to connect the buildings with our street?
Rust relies on a module tree in order to resolve all parts necessary to build.
As covered before, the (crate) root of that tree is our
src/main.rs file. So somehow, reaching out from this root, we have to tell Rust there is a (tree-)branch to add. Remembering the module-like named file — created along with the directory — we already have something to plug and play.
The referencing for a module
example_module would be like so:
Beneath “Seen as modules,” we see a difference between lines 10 and 11 in that one line has declared
pub³ and the other not. The keyword
pub tells Rust’s module system that the respective module is public to a referencing outer module, such as our crate root (
a is only visible within our
exemplary_module, whereas module
b is visible to the outside.
Note: You tell Rust to make a module, function, or symbol visible with the keyword
The module tree is obviously put together in structuring directories and their respective module-file and in writing some glue code within these files. The code, beginning in
src/auxiliary_building/auxiliary.rs, jumping to
src/auxiliary_building.rs, and ending in
src/main.rs, would be as follows:
What’s that :: thing?
A double colon separates parts of a path⁴. A path can be considered similar to resolving resources by namespaces and type names. We’ll talk about them later on in this article.
The same procedure has to be performed for our second module, or building within the street, the tower building.
The full picture
The crate root,
src/main.rs, will access the
tower_building module through
use⁶ keywords. The
tower_building module will define its parts within the
src/tower_building.rs file, and a file within the
src/tower_building (we named it
tower.rs above) will define some symbols to be used, completing the circle, in
This results in the full code as below:
What about submodules?
If we want to introduce some folder beneath
auxiliary_building, say, a
plug module, we act as before: create a folder and a file for it. The new structure would be:
Having created the file system part, we have to add some glue for the module system. As the submodule plug is added beneath the
auxiliary_building module, we have to adjust the
src/auxiliary_building.rs file accordingly. We add the plug module:
After doing so, we can use whatever is created within the
src/auxiliary_building/plug folder in our
src/main.rs. I created a function in
other_device.rs in order to demonstrate the availability:
auxiliary.rs file beneath folder
src/auxiliary_building were to evolve, you guess right: We can create a new folder, named auxiliary, split code into files, and make available public functionality and symbols in the
auxiliary.rs file. An already consuming module might have to adjust the paths accordingly.
Paths, or How to Access a Module From a Module
We used them already: Rust’s paths did do their job already, in that our crate root was able to call functionality, which was made available in our modules.
There are two types of paths: absolute and relative.
Resolving, for example, the
generate_energy() function via the path
auxiliary_building::auxiliary::generate_energy(), we used an absolute path. They always start from the perspective of the crate root, exactly where our module tree has its origin. This path was resolved from
src/main.rs. If we want to use an absolute path from anywhere within our crate, we can use the crate literal.
A relative path, on the other hand, does always take the perspective of the current module, which is the one we are writing our path in. It can be started with the self or super literals.
The literal self will start in the position of the module tree, where the current module is located. In order to navigate up one level in the tree, like in the case of a file system, we use super.
To summarize, there are three literals to start a path: crate (absolute), self, and super (relative).
The following example demonstrates the usage of all three literals:
Yes, Rust’s handling of how to add code together is different, but…
Working through this example, we saw how files can relate to modules in Rust and how Rust’s module tree resolves — or understands — other modules, defined in separated files and folder hierarchies.
Additionally, we looked into paths and how we can resolve dependencies, such as other modules, functionalities, or symbols from within a module or any other point in the project’s module tree.