Packages
When you start your project, the very first thing you will likely do is create
a new package. A package is a unit in which Rust organizes code, it consists of
metadata (such as a Cargo.toml
) and crates. You can think of it it like a
Ruby gem, a Python package, or a Node module. Packages allow you to use the
Cargo build system to compile it, run tests and manage dependencies.
A crate is a compilation unit. Unlike C, C++ or Java, which compile individual files, in Rust an entire crate is always compiled in one go. This means you don’t have to worry about the ordering of includes, and it means that all definitions are always visible. It also makes it easier for the compiler to implement certain optimizations, such as inlining code.
Contents of a Package
At the very minimum, a Rust package contains metadata (in the Cargo.toml
file) and a single library or binary crate, otherwise there is nothing to
compile. Generally, you do not need to configure Cargo to tell it where the
crates are: it automatically detects them based on their standard locations.
You can, however, override this and place your source files in non-standard
locations, but this is not recommended. For example, if you have a
src/lib.rs
file in your package, Cargo recognizes this as your library crate.
Every package needs to have either a library crate or a binary crate. It may
also contain other, supporting crates, such as integration tests, benchmarks,
examples. Having first-class support for these is a big bonus, because it means
you can run cargo test
in any Rust project and Cargo will know where the tests
are and is able to run them.
Generally, the library crate of every package is where you want to keep all of
your logic. This is because this code is what all the other crates link to by
default. So, if you write an integration test, it cannot “see” what is inside
your binary crates. In many projects, the binary crate at src/main.rs
is just
a small shell that parses command-line arguments, sets up logging and calls
into the library crate to do the hard work.
Metadata
Every crate contains some metadata, in the Cargo.toml
file. This contains everything
cargo needs to know to build the crate, such as its name, and a list of all dependencies
it needs to build. It also contains metadata neccessary for publishing it on crates.io,
Rust’s crate registry, such as its version, list of authors, license, and description.
Finally, this file can also contain metadata for other tooling, some of which we will discuss
in this book. An example file might look like this:
[package]
name = "example-package"
version = "0.1.0"
version = "MIT"
description = "My awesome crate"
authors = ["Max Mustermann <max@example.io>"]
[dependencies]
serde = { version = "1.0.12", features = ["derive"] }
anyhow = "1"
Dependencies can have optional features. This ensures a faster compilation, by only compiling them when they are explicitly enabled.
Cargo has built-in support for semantic versioning, so the versions
listed here are constraints. For example, when you specify version 1.0.12
, it really means
that your crate will work with any version >=1.0.12
and <1.1.0
, because semver considers
changes in the patch level (the third number) as non-breaking changes.
This means that when you build your crate, Rust has to resolve the version
numbers. It stores those resolved version numbers in a separate file, Cargo.lock
. This
is to ensure that you get reproducible builds: if two pepople build the project,
they always use exactly the same versions of dependencies. You have to manually tell
Cargo to go look if there are newer versions of dependencies that are within the constraints,
using cargo update
. This and some issues around it will be covered in later chapters.
Here is an example of what this looks like:
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "anyhow"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
Library and Binaries
Besides these two files, crates also contain Rust source code in various places. We will list the default locations for these here, but the locations can be configured and overridden in the metadata.
Every crate can define (at most) one library. The entrypoint for this is in src/lib.rs
.
When you use a crate as a dependency, this is what other crates can see. Even if your
project is primarily an executable and not a library, you should try to put most of the code
into this library section, because this is what is visible to example code and integration
tests. I call this library-first development.
- articles for library-first development
Besides a single library, crates can also define binaries. These must contain a
main()
function, and are compiled into executables. The default location for
binaries is src/main.rs
, and it will produce an executable with the same name as
the crate. You can create additional ones under src/bin/<name>.rs
, which will
create executables with the same name.
- graphic: executables linking against library
While Rust supports writing unit tests directly in the code, sometimes you want to
write tests from the perspective of an external user using your library (without
visibility into private functions). For this reason, you can write integration tests,
in tests/<name>.rs
. These are compiled as if they were an external crate which links
to your crate, and as such only have access to the public API.
- graphic: tests linking against library
Finally, Rust has a large focus on making it easy to write documentation. In fact,
support for generating documentation is a built-in feature. In some cases, writing code
is the best kind of documentation. For this reason, Cargo has first-class support for keeping
code examples. If you put examples into examples/<name>.rs
, they can be built and run by
cargo using cargo build --examples
and cargo run --example <name>
. There is even a
feature in Rust’s built-in support for documentation, where it will pick up and reference
examples in the code documentation automatically.
- graphic: examples linking against library
See also: Package Layout.
Creating a crate
You can use cargo new
to create an empty crate. You have the choice of creating a library
crate (using the --lib
switch) or a binary crate. Using cargo
is recommended over
creating a new crate manually, because it will usually set useful defaults.
# create a binary-only crate
cargo new example-crate
# create a library crate
cargo new --lib example-crate
This is what an example crate layout looks like, after adding some dependencies. You can see what the metadata and the source code looks like.
- src/
target/
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "example-crate"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[package]
name = "example-crate"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
serde = "1.0.205"
fn main() {
println!("Hello, world!");
}
A more full-fledged example makes use of both the library and executables, has some documentation strings, tests and examples in it, along with complete crate metadata.
Cargo has some neat features besides being able to create new crates for you.
It can also manage dependencies for you. For example, if you are inside a crate
and you would like to add serde
to the list of dependencies, you can use
cargo add
to add it:
cargo add serde --features derive
This will edit your Cargo.toml
to add the dependency, without touching
anything else. Comments and formatting is preserved. The Cargo team is quite
good at looking how people use it and extending it with functionality that is
commonly requested.
Crate Features
Rust crates can declare optional dependencies. These are additive, meaning that enabling them should not break anything. The reason for this is that Rust performs feature unification: if you have multiple dependendencies in your dependency that depend on a single crate, it will only be built once with the features unified.
- dependency tree: feature unification
This is a good way to add additional, optional features to your crates while keeping
compilation times short for those who don’t use them. If you have a dependency, you
can enable them by setting the features
key:
[dependencies]
serde = { version = "1.0.182", features = ["derive"] }
For your own crates, you can declare optional features using the features
section
in the metadata. Using features, you can enable optional dependencies, and inside your
code you can disable parts (functions, structs, modules) depending on them.
[features]
default = []
cool-feature = ["serde"]
Once you have declared a feature like this, you can use it to conditionally include code in your project, using the cfg attribute.
#![allow(unused)] fn main() { #[cfg(feature = "cool-feature")] fn only_visible_when_cool_feature_enabled() { // ... } }
Doing this can have some advantages, for example it lets you keep compilation times short for developers because they can build a subset of the project for testing purposes. However, it also requires some care, because you often need to be careful to make sure features don’t conflict with each other, see Chapter 6.10: Crate Features.
Crate Size
As mentioned earlier, in Rust a crate is a compilation unit. When you make a change in one file, the entire crate needs to be rebuilt. While it makes sense initially to start a project out with one crate, as the project grows it may make sense to split it up into multiple, smaller crates. This allows for faster development cycles.
The next section discusses how this can be done, and what mechanisms Rust supports for doing so.
Reading
Chapter 3.2.1: Cargo Targets in The Cargo Book
In this section of the Cargo book, all of the possible targets that Cargo can build for a crate are defined.
Chapter 3.1: Specifying Dependencies in The Cargo Book
In this section of the book, it is explained how dependencies are specified in Cargo.
In this article, Chris argues that it is best to default to large modules, because the cost of designing useful abstractions for the interaction is high, and it is possible to split larger modules into smaller ones later when the code is more stable.