These four words get used as if they were synonyms, including by people who should know better, and almost every structural confusion in a Rust project comes from a fuzzy version of one of them. They are worth ten minutes.
| word | what it is | how many |
|---|---|---|
| module | a namespace node inside one crate | as many as you like |
| crate |
one compilation unit — one rustc invocation |
one per rustc run |
| package |
one Cargo.toml |
at most one library, any number of binaries |
| workspace |
a set of packages sharing one Cargo.lock and one target/ |
one per repo, usually |
Crate: the compilation unit
A crate is what rustc compiles in one go. It has exactly one root module —
src/lib.rs for a library, src/main.rs for a binary — and every module in
the crate descends from it. When you write pub(crate), this is the crate
you mean. When the compiler says “cannot be re-exported outside the crate”,
this is the boundary.
Crucially: the crate is also the privacy boundary and the SemVer boundary.
Everything from 10.3 and 10.4 is scoped to it. #[non_exhaustive] does
nothing inside it. The orphan rule is stated in terms of it.
Package: the unit Cargo manages
A package is a directory with a Cargo.toml. It is the unit of publishing, of
versioning and of dependency declaration. And here is the fact that surprises
everybody:
A package containing both
src/lib.rsandsrc/main.rscompiles TWO crates.
They are separate compilation units. The binary does not automatically see the
library’s internals — it has to use the library by its package name,
exactly as an external user would:
// src/main.rs, in a package named my-tool
use my_tool::run; // note the underscore
fn main() {
run();
}
💡Your package is called image-proc. src/lib.rs has a pub(crate) fn decode. Why can src/main.rs not call it, and what are your two options?
click to reveal
Because src/main.rs is a different crate. pub(crate) means “visible
within the crate that declares it”, and the binary is not that crate — from
its point of view the library is an external dependency that happens to live
in the same directory.
Your options are:
-
Make
decodegenuinelypub, accepting that it is now public API that external users can call and that you owe SemVer guarantees on. -
Move whatever the binary needed into the library as a thin
pubentry point, and keepdecodeinternal. This is the usual answer and it is why the standard advice is logic insrc/lib.rs,src/main.rsa thin shell.
Option 2 has a second payoff, covered in 10.21: integration tests in tests/
are separate crates too, so a binary-only package cannot be integration-tested
at all. Putting the logic in the library is what makes it testable.
Also note image-proc became image_proc in the use. Cargo normalises
dashes to underscores because a dash is not a valid Rust identifier. The
package is named one way and the crate the other, which is a small, permanent
papercut.
Module: a node inside one crate
Modules are what the rest of this track has been about. They do not cross crate
boundaries; mod never reaches into a dependency. If you find yourself
wanting mod some_dependency;, what you want is use some_dependency::… —
the dependency is already linked, its name is already in the extern prelude,
and no mod is involved.
This is the source of “why did adding a file do nothing”. A .rs file in
src/ that no mod statement names is not part of any crate. rustc never
opens it. Your editor will happily show you the code, your changes will
compile-check in rust-analyzer, and nothing will happen.
Workspace: shared lockfile, shared target directory
A workspace groups packages so they resolve dependencies together and share
one target/. That is the whole idea. The members are still separate packages
producing separate crates; what they share is the resolution and the build
cache.
The practical consequences are covered in 10.18, but two of them are worth previewing because they surprise people:
- Feature unification (10.16) happens across the workspace, so a dev-dependency of one member can enable features in a crate another member depends on.
-
[profile.*]and[patch]are root-only. Putting[profile.release]in a member’s manifest is silently ignored with a warning, and people lose an afternoon to it.
extern crate is a fossil
In edition 2015 you had to write extern crate serde; to bring a dependency
into scope. Since 2018 dependencies are in the extern prelude automatically and
the keyword is dead — with two survivals:
-
#[macro_use] extern crate foo;for pre-2018 macro export. -
extern crate alloc;inno_stdcrates, whereallocis not in the prelude and has to be pulled in explicitly.
If you see extern crate in a tutorial, the tutorial predates 2018, which
tells you something about the rest of it too.
The short version
-
Module: a namespace node inside a crate. Declared with
mod, never crosses crates. -
Crate: one
rustcinvocation. The privacy, SemVer and orphan-rule boundary. -
Package: one
Cargo.toml. Up to one library plus any number of binaries — so a package can be several crates. -
Workspace: several packages, one lockfile, one
target/, one dependency resolution.