Code Organization
Code Organization for a Simple Library
A simple library crate may consist of its crate root (e.g. the lib.rs
file) and several modules in separate files.
- src
- lib.rs
- module1.rs
- module2.rs
- ...
Cargo.toml
where lib.rs
includes the modules with mod
statements:
pub mod module1; // Public modules, so that the contents may be accessible from outside the crate.
pub mod module2;
// ...
You may also keep the modules private and reexport specific items (or modules) with a pub use
statement:
mod module1;
mod module2;
pub use module1::public_function;
pub use module2::Struct1;
It is common for lib.rs
to only contain mod
and pub use
statements, and nothing else.
In Rust, there are no requirements to store a single struct
or enum
and associated code per file.
On the contrary, it is idiomatic for a module to contain multiple functions, struct
or enum
declarations, and impl
blocks with related functionality. For example, you may write all configuration-related items in the config
module.
Code Organization for a Complex Library
As needs grow, you may create nest modules:
- src
- lib.rs
- module1/ # First-level module.
- mod.rs
- submodule1.rs # Nested submodule.
- submodule2.rs # Nested submodule.
- module2.rs
- ...
Cargo.toml
where mod.rs
contains:
pub mod submodule1;
pub mod submodule2;
// ...
Code elsewhere can then refer to public items in the submodules:
// In `module2.rs`:
use super::module1::submodule1;
use super::module1::submodule2::Struct1;
fn a_func() {
submodule1::public_function();
let _struct = Struct1;
}
Nested folders are also possible:
- src
- lib.rs
- module1/
- mod.rs
- submodule1/
- mod.rs
- module2.rs
- ...
Less Common Code Organizations
Less commonly, you may see:
- src
- lib.rs
- module1.rs
- module1/
- submodule1.rs
- submodule2.rs
- module2.rs
- ...
where the module is a file and its submodules are under a folder named after the module.
Finally, module1
can also be defined inline in lib.rs
- especially if the module only contains mod
/ pub use
statements.
mod module1 {
pub mod submodule1;
pub mod submodule2;
}
Flatten the Module Hierarchy using Reexports
Instead of working with deeply-nested modules, it is convenient to keep submodules private and reexport items of interest instead.
For example, you may write in module1
:
mod submodule1;
mod submodule2;
// Reexport a function and a struct.
pub use submodule1::public_function;
pub use submodule2::Struct1;
This flattens the module hierarchy and hides implementation details. Code in a parent module then refers to:
use module1::public_function; // Appears as if it were defined in `module1`.
use module1::Struct1;
Code Organization for Binary Crates
Similarly, a simple binary crate may consist of a crate root (e.g. main.rs
) and several modules in separate files and/or folders.
- src
- main.rs
- module1.rs
- module2
- mod.rs
- submodule1.rs
- submodule2.rs
Cargo.toml
Commonly, main.rs
is kept minimal and refers to a library crate in the same src
folder:
- src
- lib.rs # Library crate root.
- main.rs # Binary crate root.
- module1.rs # Module of the library.
- ...
- tests
- examples
Cargo.toml
In that case, main.rs
imports the contents of the library crate via use crate_name::module_name;
and only contain a short main()
function. This code organization exposes a public library crate API, which has the advantages of being more easily testable (via integration tests in the tests
folder). However, if the crate is published on crates.io
, you must make sure to update your crate version according to Cargo's SemVer (semantic versioning) rules, every time you change the (now public) API. Your code will also break if you decide to rename your crate.
A variation of this code organization puts the command-line argument parsing (or UI) code in a module under main.rs
and keeps the non-CLI code in the associated library crate.
mod cli; // Command-line argument parsing.
use crate_name::lib_module::*; // Business logic in the library crate.
fn main() {
// ...
}
Organize Large Projects using a Workspace
If your project is huge, you may want to split it into several crates, which you then depend on in your main project. For example, you may create a xyz-core
crate, a xyz-derive
crate for procedural macros that let you #[derive(...)]
traits defined in your core crate, a xyz-utils
crate, and a xyz
main crate that binds all subcrates together.
Each crate, of course, should be further split into modules (and submodules) as described above.
You will most often create a 'Cargo workspace' to tie together your project's crates. A 'workspace' is a set of 'packages' developed in tandem that share the same Cargo.lock
and output (e.g. target
) directory - and therefore share the same dependencies. A package is a bundle of one or more crates with Cargo.toml
file that describes how to build those crates. A package include at least one crate, as many binary crates as you like, but at most only one library crate. Note that the concept of a 'package' is often conflated with that of a 'crate' and the latter word is often used to describe the former. Practically, a package is a subfolder of your workspace that contains a Cargo.toml
file.
A typical organization may look as follows:
# The root folder of your workspace.
- lib1/ # First package subfolder (library crate).
- src/
- lib.rs
- ...
- tests/ # Optional integration tests and examples.
- examples/
- Cargo.toml
- lib2/ # Second package (library crate + several optional binary crates).
- src/
- lib.rs
- bin
- tool1.rs # Optional binary, perhaps a tool for e.g. database migration.
- tool2.rs
- ...
- Cargo.toml
- main_lib
- src/
- lib.rs # Often, a main library crate that reexports individual libraries.
- main.rs # Optional binary that uses the library, e.g. a CLI tool.
- ...
- ...
- target/ # Shared output directory.
- Cargo.toml # Workspace Cargo.toml that references the packages.
- Cargo.lock # Shared lock file (and dependencies).
You may use feature flags in the main library crate's Cargo.toml
to selectively build subcrates and their dependencies:
[dependencies]
lib1 = { path = "../lib1" }
lib2 = { path = "../lib2", optional = true }
[features]
feature2 = ["dep:lib2"]
In projects that mix multiple technologies (a web project or a mdbook
that combines markdown and Rust code, like this book), it is common to create a "crates" subdirectory that contains all Rust packages.
The main Cargo.toml
file in the root of the workspace should contain a 'workspace' section that references its packages:
[workspace]
members = [ "lib1", "lib2", "main_lib" ]
Confusingly, a workspace Cargo.toml
can also include a 'root package' in addition to members. That lets you place the code of the main library or executable in e.g. a src
folder directly under the workspace root.
Related Topics
- Package Layout.