Crates and Modules
Crates
The easiest way to see what crates are and how the work is to use cargo build
with the --verbose
flat to build an existing project that has some dependencies.
When compiling libraries, Cargo uses the --crate-type lib
option. This tells rustc
not to look for a main()
function but instead to produce a .rlib file containing compiled code in a form that later rustc
commands can use as input.
When compiling a program, Cargo uses --crate-type bin
, and the result is a binary executable for the target platform.
With each rustc
command, Cargo passes --extern
options giving the filename of each library the crate will use. This ties directly into the extern crate some_crate;
statements in source code.
Command: The command
cargo build --release
will produce an optimized release build.
Qualities of a release build:
- Run faster
- Compile slower
- Don't check for integer overflow
- Skip
debug_assert!()
assertions - Less reliable and verbose stack traces
Build Profiles
The following CLI commands are used to select a rustc
profile:
Command | Cargo.toml section used |
---|---|
cargo build | [profile.debug] |
cargo build --release | [profile.release] |
cargo test | [profile.test] |
Behavior: If no profile is specified,
[profile.debug]
is selected by default.
Tip: To get the best data from a profiler, you need both optimizations and debug symbols to be enabled. To do so, add this to your cargo config:
[profile.release] debug = true # enable debug symbols in release builds
Modules
Concept: Modules are Rust's namespaces. Whereas crates are about code sharing between projects, modules are about code organization within a project.
Term: A modules is a collection of items.
Behavior: Any module item not marked
pub
is private.
Behavior: Modules can be nested. It's common to see a module that's a collection of submodules:
#![allow(unused)] fn main() { mod life { pub mod animalia { pub mod mammalia {} } pub mod plantae {} pub mod fungi {} pub mod protista { pub mod archaea {} pub mod bacteria {} } }
It's generally advised not to keep all source code in a single massive file of nested modules. For obvious reasons.
Modules in Separate Files
Behavior: Writing a module inline like
mod life;
tells the compiler that thelife
module lives in a separate file calledlife.rs
.
When you build a Rust crate, you're recompiling all of its modules, regardless of where those modules live.
Behavior: A module can have its own directory. When Rust sees
mod life;
, it checks for bothlife.rs
andlife/mod.rs
.
Concept: A
mod.rs
file is exactly like a barrel (index.js) in JS/TS.
Paths and Imports
Operator: The
::
operator is used to access the items of a module. e.g.life::animalia::mammalia::...
.
Paths to items can be either relative or absolute. An absolute path is prefixed by ::
and can be used to access "global" items, e.g. ::std::mem::swap
is an absolute path.
Concept: Accessing an absolute path is a lot like accessing the global object in JS, ie
window
, in a browser.
Operator: The
use
declaration creates aliases to modules and items through the enclosing block or module. e.g.use std::mem;
create a local alias to::std::mem
's items.
It's generally considered best style to import types, traits, and modules, then use relative paths to access the items within them.
Several items from the same module can be imported at once, as can all items:
#![allow(unused)] fn main() { use std::collections::{HashMap, HashSet}; // import just two items use std::io::prelude::*; // import all items }
Operator: Modules do not automatically inherit items from their parent modules. The
super
keyword can be as an alias for the parent module, andself
is an alias for the current module.
Submodules can access private items in their parent modules, but they have to import them by name. use super::*;
will only import the pub
items.
Modules aren't the same thing as files, but there some analogies between module paths and file paths:
Module path | File path | Description |
---|---|---|
self | "." | Accesses the current module |
super | ".." | Accesses the parent module |
extern crate | Similar to mounting a filesystem |
The Standard Prelude
Implicit Behavior: The standard library
std
is automatically linked with every project, as are some items from the standard prelude likeVec
andResult
. It's as though the following imports are invisibly added to all files:#![allow(unused)] fn main() { extern crate std; use std::prelude:v1::*; }
Convention: Naming a module
prelude
tells users that it's meant to be imported using*
.
Items: The Building Blocks of Rust
Items make up the composition of modules. The list of items is really a list of Rust's features as a language:
Items | Keywords |
---|---|
Functions | fn |
Types | struct , enum , trait |
Type aliases | type |
Methods | impl |
Constants | const , static |
Modules | mod |
Imports | use , extern crate |
FFI blocks | extern |
Item: Types
User-defined types are introduced using struct
, enum
, and trait
keywords.
A struct's field, even private fields, are accessible through the module where the struct is declared. Outside of the module, only pub
fields are visible.
Item: Methods
An impl
block can't be marked pub
; rather its methods can be marked pub
individually.
Private methods, like private struct fields, are visible throughout the module where they're declared.
Item: Constants
The const
keyword introduces constant. const
syntax is just like let
, except that the type must be defined, and it may or may not be marked pub
.
Convention:
UPPER_CASE_NAMES
are conventional for naming constants.
Concept: A
const
is a bit like the#define:
preprocessor directive in C++, and as such they should be used for specifying magic numbers and strings.
Item: Imports
Even though use
and extern crate
declarations are just aliases, they can also be marked pub
. In fact, the standard prelude is written as a big series of pub
imports.
Item: FFI blocks
extern
blocks declare a collection of functions written in some other language so that they can be called from Rust.
Turning a Program into a Library
These are roughly the steps to convert a program into a library:
- Change the name of
src/main.rs
tosrc/lib.rs
- Add the
pub
keyword to public features of the library. - Move the
main
function to a temporary file somewhere.
Term: The code in
src/lib.rs
forms the root module of the library. Other crates that use your library can only access the public items of this root module.
The src/bin Directory
Cargo has built-in support for small programs that live in the same codebase as a library.
The main()
function that we stowed away in the above steps for converting our code to library should be moved to a file named src/bin/my_program.rs
. The file needs to then import the library as it would any other crate:
#![allow(unused)] fn main() { extern crate my_library; use my_library::{feature_1, feature_2}; }
That's it!
Attributes
Tests and Documentation
Tests are just ordinary functions marked with the #[test]
attribute.
To test error cases, add the #[should_panic]
attribute to your test. This tells the compiler that we expect this test to panic:
#![allow(unused)] fn main() { #[test] #[should_panic(expected="divide by zero")] fn test_divide_by_zero_error() { 1 / 0; } }
Convention: When your tests gete substantial enough to require support code, the convention is to put them in a
tests
module an declare the whole module to be testing-only using the#[cfg(test)]
attribute.
Integration Tests
Term: Integration tests are
.rs
files that live in atests
directory alongside your project'ssrc
directory. When you runcargo test
, Cargo compiles each integration test as a separate, standalone crate, linked with your library and the Rust test harness.
Since integration tests use your program as if it were a separate crate, you must add extern crate my_library;
to them.
Documentation
Come back to this
Doc-Tests
Come back to this
Specifying Dependencies
Generally in a Cargo.toml, you're used to seeing items in the [dependencies]
section that are specified by version number and look like this:
[dependencies]
num = "0.1.42"
The above convention is fine, but it only allows use of crates published on crates.io.
Remote Git Dependencies
To use a dependency by referencing a git repo, specify it like this:
my_crate = { git = "https://github.com/Me/my_crate.git", rev = "093f84c" }
Local Dependencies
To use a dependency by referencing a local crate, specify it like this:
my_crate = { path = "../path/to/my_crate" }
Versions
Come back to the this.
Cargo.lock
Cargo upgrades dependencies to newer version only when you tell it to using cargo update
, in which case it only upgrades to the latest dependency versions that are compatible with what's specified in Cargo.toml.
Publishing Crates to crates.io
The command cargo package
creates a file containing all your library's source files, including Cargo.toml, which is what will be uploaded to crates.io.
Before publishing, you have to log in locally using cargo login <API key>
. Go here to get an API key.
Workspaces
Given a root directory that contains a collection of crates, you can save compilation time and disk space by creating workspace.
All that's needed is a Cargo.toml file in the root directory:
[workspace]
members = ["my_first_crate", "my_second_crate"]
With that you're free to delete any Cargo.lock
and target
directories that exist in the subdirectories. All Cargo.lock
and compiled resources will all be grouped at a single location in the root directory.
With workspaces, cargo build --all
in any crate will build all crate in the root directory. The same goes for cargo test
and cargo doc
.