Rust modules confusion: how main.rs and lib.rs drive you crazy (and how to fix it)
\n\nHave you ever written in Rust, created a new project, added main.rs and lib.rs — and suddenly the compiler started throwing errors that seemed to make no sense? Sound familiar? If you've Googled "Rust modules confusion when there is main.rs and lib.rs", you are not alone. This question on Stack Overflow has thousands of views, and for good reason. Module confusion is one of the most common problems for beginners (and even experienced developers transitioning from other languages).
In this article, we will not just break down how the Rust module system works. We will show how its proper organization directly affects the optimization and performance of your code. You will learn why lib.rs is not just "another file", but a powerful tool for speeding up compilation and creating a clean API. Let's go!
The Problem: "Why doesn't the compiler see my modules?"
\n\nImagine: you created a project my_project using cargo new my_project. By default, Cargo creates src/main.rs. You add a second file — src/lib.rs — and write a function in it. Then you try to call it from main.rs — and you get an error:
error[E0432]: unresolved import `my_project::some_function`\n --> src/main.rs:1:5\n |\n1 | use my_project::some_function;\n | ^^^^^^^^^^^^^^^^^^^^^^^^^ no `some_function` in the root\n\nFamiliar pain? What's the matter? It's simple: Rust considers main.rs and lib.rs as two different crate roots. If not specified otherwise in Cargo.toml, Cargo automatically creates two crates: a binary one (from main.rs) and a library one (from lib.rs). They don't "see" each other directly until you explicitly specify dependencies.
This is that very Rust modules confusion everyone talks about. But instead of getting angry, let's turn this "feature" into an advantage.
\n\nThe Solution: How to properly organize modules
\n\nThere are two approaches, and the choice depends on your goal. But we'll give you a spoiler right away: the second approach is better for performance.
\n\nOption 1: Everything in one crate (simple, but not scalable)
\n\nIf your project is small, you can declare modules inside main.rs using mod:
// src/main.rs\nmod utils; // looks for src/utils.rs or src/utils/mod.rs\n\nfn main() {\n utils::do_something();\n}\n\nThis works, but if you add lib.rs, confusion begins. lib.rs will become a separate crate, and modules from main.rs will not be available in it. Fine for small scripts, not for serious projects.
Option 2: Library + binary (the right path for optimization)
\n\nThe most competent way is to make lib.rs the main storage for logic, and main.rs a thin wrapper for execution. This gives you:
- \n
- Modularity: all business logic in the library. \n
- Testability: the library can be tested separately. \n
- Performance: the compiler optimizes library code better if it is used repeatedly. \n
How to do it? Simply:
\n// src/lib.rs\npub mod utils;\n\npub fn run() {\n utils::do_something();\n}\n\n// src/main.rs\nfn main() {\n my_project::run(); // my_project — the crate name from Cargo.toml\n}\n\nNote: main.rs now uses my_project::run(). This works because Cargo sees lib.rs as a library named my_project. Voila — the confusion is gone!
Optimization and performance: why lib.rs is your friend
\n\nNow the most inte