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 --releasewill 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
pubis 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 thelifemodule 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.rsandlife/mod.rs.
Concept: A
mod.rsfile 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
usedeclaration 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
superkeyword can be as an alias for the parent module, andselfis 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
stdis automatically linked with every project, as are some items from the standard prelude likeVecandResult. 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
preludetells 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_NAMESare conventional for naming constants.
Concept: A
constis 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.rstosrc/lib.rs - Add the
pubkeyword to public features of the library. - Move the
mainfunction to a temporary file somewhere.
Term: The code in
src/lib.rsforms 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
testsmodule an declare the whole module to be testing-only using the#[cfg(test)]attribute.
Integration Tests
Term: Integration tests are
.rsfiles that live in atestsdirectory alongside your project'ssrcdirectory. 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.