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:

CommandCargo.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 the life module lives in a separate file called life.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 both life.rs and life/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, and self 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 pathFile pathDescription
self"."Accesses the current module
super".."Accesses the parent module
extern crateSimilar 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 like Vec and Result. 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:

ItemsKeywords
Functionsfn
Typesstruct, enum, trait
Type aliasestype
Methodsimpl
Constantsconst, static
Modulesmod
Importsuse, extern crate
FFI blocksextern

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:

  1. Change the name of src/main.rs to src/lib.rs
  2. Add the pub keyword to public features of the library.
  3. 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

Click here.

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 a tests directory alongside your project's src directory. When you run cargo 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.