main.rs and lib.rs
In a Cargo package, src/main.rs and src/lib.rs are the two primary crate root files. src/main.rs is the entry point for compiling an executable binary crate, while src/lib.rs is the entry point for compiling a reusable library crate.
Roles of src/main.rs vs src/lib.rs
| Property | src/main.rs (Binary Crate Root) |
src/lib.rs (Library Crate Root) |
|---|---|---|
Crate type (--crate-type) |
bin (executable program) |
lib / rlib (reusable library) |
Requires fn main()? |
Yes (E0601 if missing) |
No (main has no special meaning in lib.rs) |
| Can other packages depend on it? | No (external crates cannot import items from a binary crate) | Yes (pub items in lib.rs form the package's public API) |
Can integration tests (tests/*.rs) import it? |
No | Yes (integration tests import the library crate by name) |
| Maximum count per Cargo package | 1 default (src/main.rs) + any number in src/bin/*.rs |
At most 1 (src/lib.rs) per package |
Using Both src/lib.rs and src/main.rs in the Same Package
A single Cargo package can contain both src/lib.rs and src/main.rs simultaneously! When both files are present, Cargo builds two separate crates that share the package's name:
- First, Cargo compiles
src/lib.rsas a library crate. - Next, Cargo compiles
src/main.rsas a binary crate that automatically has the library crate available as a dependency under the package's name.
my_scanner/ (Cargo Package)
├── Cargo.toml
└── src/
├── lib.rs ---> Compiles to library crate `my_scanner` (core logic & public types)
└── main.rs ---> Compiles to binary executable `my_scanner` (CLI parsing + calls `my_scanner::...`)
Why the Dual lib.rs + main.rs Pattern Is Idiomatic
Even when building a standalone command-line application, experienced Rust developers place the core data structures and business logic in src/lib.rs and keep src/main.rs as a thin wrapper (handling only CLI argument parsing, configuration loading, and exit codes):
// src/lib.rs — Library crate (`word_counter`)
pub struct Stats {
pub lines: usize,
pub words: usize,
}
pub fn analyze(text: &str) -> Stats {
Stats {
lines: text.lines().count(),
words: text.split_whitespace().count(),
}
}
// src/main.rs — Binary crate (`word_counter`)
use word_counter::analyze; // Imports the sibling library crate `src/lib.rs`!
fn main() {
let sample = "Rust separates library logic\nfrom binary entry points.";
let stats = analyze(sample);
println!("Lines: {}, Words: {}", stats.lines, stats.words);
}
Lines: 2, Words: 8
Structuring an application this way provides three immediate benefits:
- Integration testing (
tests/): Files in thetests/directory can only import a package's library crate (src/lib.rs), neversrc/main.rs. Moving logic intosrc/lib.rsmakes the entire application testable via integration tests and doctests. - Code reuse across multiple binaries: If you add helper binaries in
src/bin/, every binary can import shared functions fromsrc/lib.rswithout duplicating code. - Fast compilation checks: Library API boundaries stay clean and separated from CLI I/O concerns.
Avoid Declaring the Same Module in Both lib.rs and main.rs
A common beginner mistake in a package with both src/lib.rs and src/main.rs is writing mod helper; in both src/lib.rs and src/main.rs:
❌ Anti-Pattern (Compiles `helper.rs` TWICE as two distinct types!):
src/lib.rs ---> `pub mod helper;`
src/main.rs ---> `mod helper;` (Creates a duplicate `main::helper` module!)
✅ Correct Pattern (Declare `pub mod helper;` in `lib.rs`, and `use` it in `main.rs`):
src/lib.rs ---> `pub mod helper;`
src/main.rs ---> `use my_pkg::helper;`
If mod helper; appears in both src/lib.rs and src/main.rs, rustc compiles src/helper.rs twice—once inside the library crate and once inside the binary crate—causing dead-code warnings and distinct incompatible types (my_pkg::helper::Config vs crate::helper::Config). Always declare shared modules in src/lib.rs with pub mod and import them into src/main.rs with use <pkg_name>::<module>;.