A crate, a module and separate compilation
C++ splits a program into translation units, headers and a linker step. Rust splits it into crates and modules, and there is no header and no separate link command: a crate is one compilation unit that may read many files, and a module is a namespace inside it.
This matters early because a single file stops being enough after a few hundred lines, and because privacy in Rust is a module rule: a name is private to its module unless it is marked pub. The two errors at the end of the lesson are the ones this rule produces most often.
One file is a crate
A crate is the unit rustc compiles: one root file plus every file it pulls in. The root is main.rs for a binary and lib.rs for a library. Modules can live in the same file with a block, which is how a small helper stays next to its caller without a second file.
The module below has two public functions and no state. Inside the module they call each other by bare name; outside it the path is math::square. The module name is not a type and takes no storage: it is a namespace and a privacy boundary.
mod math {
pub fn square(n: i32) -> i32 {
n * n
}
pub fn cube(n: i32) -> i32 {
n * n * n
}
}
fn main() {
println!("square: {}", math::square(7));
println!("cube: {}", math::cube(7));
}
rustc --edition 2024 inline.rs
./inline
square: 49
cube: 343
Nothing here is compiled twice and nothing is linked by name, because rustc sees the whole crate at once. That is also why a function can be called before its definition appears in the file: order inside a module does not matter.
Separate files
mod math; with a semicolon instead of a block tells the compiler to read math.rs beside the root file. The file contains the items of the module, not a module wrapper, and the same rule applies one level down: mod tools; inside math.rs reads math/tools.rs.
Both files are below. The root declares the module and calls into it, and the module marks its two functions pub. Without that keyword the functions would be private to math and the root could not call them, which is the next section.
mod math;
fn main() {
println!("square: {}", math::square(6));
println!("cube: {}", math::cube(3));
}
pub fn square(n: i32) -> i32 {
n * n
}
pub fn cube(n: i32) -> i32 {
n * n * n
}
rustc --edition 2024 main.rs
./main
square: 36
cube: 27
The build takes one command and produces one binary, and there is no object file to name. Incremental builds are cargo's job rather than the programmer's, and the lesson on cargo build shows what it caches.
pub and privacy
Everything is private by default, including a function inside a module that the root owns. A private function is visible inside its module and its children, and nowhere else. Calling it from the root is E0603, and the message names both the visibility and the definition, so the fix is one keyword in one place.
The same rule covers fields, structs and modules. pub(crate) opens an item to the whole crate but not to other crates, which is the usual choice for a helper that other modules need and users do not.
pub fn square(n: i32) -> i32 {
n * n
}
fn hidden() -> i32 {
0
}
mod math;
fn main() {
println!("{}", math::hidden());
}
rustc --edition 2024 main.rs
error[E0603]: function `hidden` is private
--> main.rs:4:26
|
4 | println!("{}", math::hidden());
| ^^^^^^ private function
|
note: the function `hidden` is defined here
--> math.rs:5:1
|
5 | fn hidden() -> i32 {
| ^^^^^^^^^^^^^^^^^^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0603`.
A module declared pub mod math is not enough: each item inside it has its own visibility, and making the module public only lets other modules name it.
use and paths
A path starts at the crate root with crate::, at the current module with self::, or at the parent with super::. A name written without a prefix is looked up in the current module and then in the prelude, which is why String and println! work without an import.
use binds a path to a short name for the rest of the module. It does not load anything: the module must already be part of the crate, and importing a name that does not exist is E0432. The example nests two modules to show the full path in the import and the bare name at the call.
mod text {
pub mod greeting {
pub fn hello(name: &str) -> String {
format!("Hello, {name}")
}
}
}
use text::greeting::hello;
fn main() {
println!("{}", hello("Rust"));
}
rustc --edition 2024 nested.rs
./nested
Hello, Rust
The failing version imports a name that the module does not define. The message is short: the path, the missing part and the module it was looked for in. It is the first thing to check when a file was renamed and one import was left behind.
mod math;
use math::power;
fn main() {
println!("{}", power(2, 8));
}
pub fn square(n: i32) -> i32 {
n * n
}
rustc --edition 2024 main.rs
error[E0432]: unresolved import `math::power`
--> main.rs:3:5
|
3 | use math::power;
| ^^^^^^^^^^^ no `power` in `math`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0432`.
A missing file is a third error, E0583, and it appears when mod math; has no math.rs beside it. The compiler looks for math.rs and math/mod.rs and names both candidates only when neither exists.
mod math;
fn main() {
println!("{}", math::square(2));
}
rustc --edition 2024 missing_module.rs
error[E0583]: file not found for module `math`
--> missing_module.rs:1:1
|
1 | mod math;
| ^^^^^^^^^
|
= help: to create the module `math`, create file "math.rs" or "math/mod.rs"
= note: if there is a `mod math` elsewhere in the crate already, import it with `use crate::...` instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0583`.
The same with cargo
A cargo package keeps the root at src/main.rs and every module beside it, so the same mod math; reads src/math.rs. The edition comes from the manifest instead of the command line, and the build output goes to target/.
The example uses both a full path and an import to show that they are the same thing: crate::math::square and square after the use. The file list is the whole package apart from the build directory and the lock file, which cargo writes on the first build.
project/Cargo.lock
project/Cargo.toml
project/src/main.rs
project/src/math.rs
mod math;
use crate::math::square;
fn main() {
println!("square: {}", square(9));
println!("cube: {}", math::cube(2));
}
pub fn square(n: i32) -> i32 {
n * n
}
pub fn cube(n: i32) -> i32 {
n * n * n
}
cargo new --vcs none project
cd project
cargo run
Compiling project v0.1.0 (/tmp/rw/L006/project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/project`
square: 81
cube: 8
The lesson on workspaces takes the next step: several crates in one repository, each with its own root, built by one command.
Exercises
- Add a third function to inline.rs that calls square from inside the module. Does it need math:: in front of the name?
- Make the private example compile by changing one keyword. Which visibility would you choose for a helper that only this crate uses?
- Move math.rs into a folder as math/mod.rs and rebuild. Does anything in the root file change?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a crate in Rust?
Answer
One compilation unit: a root file and every module it pulls in
rustc compiles a crate, and cargo packages one or more crates with a manifest.
-
What does
mod math;do?Answer
It declares a module whose items are read from math.rs
The semicolon form loads the file as the body of the module, in the same crate.
-
What does
pubon a function do?Answer
It makes the function visible outside its module
Items are private by default, and pub widens the visibility to the enclosing module and beyond.
-
What does the path
crate::at the start of ausemean?Answer
The crate that is being compiled, from its root
crate:: is an absolute path inside the current crate, unlike self:: and super::.
-
What does this program print?
mod math { pub fn square(n: i32) -> i32 { n * n } } fn main() { println!("{}", math::square(9)); }Answer
81
square multiplies the argument by itself, and nine times nine is eighty-one.
-
What does this program print?
mod outer { pub mod inner { pub fn double(n: u32) -> u32 { n * 2 } } } use outer::inner::double; fn main() { println!("{}", double(21)); }Answer
42
Both modules and the function are public, so the import reaches the function and it doubles the argument.
-
What is the first line of the compiler output for this program?
mod math { fn hidden() -> i32 { 0 } } fn main() { println!("{}", math::hidden()); }Answer
error[E0603]: function
hiddenis privateThe function exists in the module but is not public, and the error names the privacy.
-
What is the first line of the compiler output for this program?
mod math { pub fn square(n: i32) -> i32 { n * n } } use math::power; fn main() { println!("{}", power(2, 8)); }Answer
error[E0432]: unresolved import
math::powerThe import names something the module does not contain, and the message says no power in math.
-
What is the first line of the compiler output for this program?
mod math; fn main() { println!("{}", math::square(2)); }with no math.rs in the folder.Answer
error[E0583]: file not found for module
mathThe module file is missing, so the compiler stops before it can look inside the module.
-
What does this program print?
mod text { pub fn shout(word: &str) -> String { word.to_uppercase() } } fn main() { println!("{}", text::shout("rust")); }Answer
RUST
to_uppercase returns a new String with every letter upper case, and shout is public.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev