Intro
This post corresponds to "Chapter 8. Crates and Modules" in the book.
Crates
Crates are the package format for Rust; they're made up of
source code plus tests, data, etc. Managing those
packages is the job of cargo, came across this previously. Building
crates is triggered by running cargo build
, which downloads (possibly
transitive) deps mentioned in Cargo.toml and runs rustc on them. Rustc
takes --crate-type lib
or --crate-type bin
options for building libraries
or programs. With libraries it will create an .rlib file, otherwise it
will look for a main()
func and spit out a
program. Code from .rlib files will be statically linked into the
program. With cargo build --release
I can
get an optimized build.
Editions
Rusts compat promises are built around "editions", e.g. the 2018 edition added the async/await keywords. The desired edition is specified at the top of Cargo.toml. The promise is that rustc will always support all editions; and crates can mix editions, i.e. depend on crates that have specific edition requirements – crates only need to upgrade to newer editions for new lang features.
Build Profiles
Builds are configured by build profiles. The book recommends enabling debug symbols for the release profile for running the binary under a profiler (so I'd get both optimizations and debug syms).
Modules
Modules are Rusts namespaces. Symbols marked 'pub' are exported, otherwise they're private to the module; a 'pub crate' will be private to the crate. There are some more variants on controlling visibility: 'pub(super)' to make an item visible to the parent, 'pub(in <path>)' for visibility in a specific subtree.
Nesting and Code organization
Modules can be nested. Modules can be defined along the code in a single file, but also (and imho more practical) a module body can live in a separate file or a separate directory too.
Example: module foo with code in a separate file:
main.rs, contains declaration
pub mod foo;
foo.rs, with code for module foo
Example: module bar and quux with code in a separate directory:
main.rs, contains declaration
pub mod bar;
bar/mod.rs, contains declaration
pub mod quux;
bar/quux.rs, with code for module quux
The compiler will look for a bar.rs or a bar/mod.rs
It's also possible to have a directory 'foo' plus a 'foo.rs' with declarations for nested modules:
foo.rs, contains declaration
pub mod bar
foo/bar.rs with code body for the bar module
Paths and Imports
The ::
operator accesses symbols of a
module. For instance to use the swap fun in the std::mem module, use:
std::mem::swap(...)
To shorten this, import the module:
use std::mem;
mem::swap(...)
It'd be possible to directly import swap() as well – but importing types, traits and modules, and then accessing other items from there is considered better style.
This imports several names and renames imports respectively:
use std::fs::{self, File}; // import both `std::fs` and `std::fs::File`.
use std::io::Result as IOResult;
Importing from the parent must be done explicitly with the super
keyword: use super::AminoAcid;
Referring to the top level with the crate keyword: use crate::proteins::AminoAcid;
To refer to an external crate (in case of naming conflicts), use the
absolute path starting with colons: use ::image::Pixels;
The Standard Prelude
There's two ways in which the standard lib is automatically
available. Firstly, the std
lib is always
linked.
Secondly, some items are always auto-imported, as if every module started with:
use std::prelude::v1::*;
This makes some things always available, like Vec
or Result
Some external crates contain modules named prelude
, that's a convention to signal it's
meant to be imported with *
Making use Declarations pub
Importing with use
can be made public
and thus re-export. This will make Leaf and Root available at the
top:
// mod.rs
pub use self::leaves::Leaf;
pub use self::roots::Root;
Making Struct Fields pub
Visibility can also be controlled for individual fields:
pub struct Fern {
pub roots: RootSet,
pub stems: StemSet
}
Statics and Constants
pub const ROOM_TEMPERATURE: f64 = 20.0; // degrees Celsius
pub static STATIC_ROOM_TEMPERATURE: f64 = 68.0; // degrees Fahrenhe
Constants are similar to #define
, while
statics are variables that live through the program lifetime. Use
statics for larger amounts of data, or if you need a shared ref. Statics
can be mut
but those can't be accessed in
safe code; they're of course non-thread-safe.
Turning a Program into a Library
By default, cargo looks for a src/lib.rs
and if it finds one, builds a
library
The src/bin directory
Cargo treats src/bin/*.rs
as programs
to build, as well as src/bin/*/*.rs
files
Attributes
Things like controlling compiler warnings or conditional compilation are done via attributes.
Example: quell warnings about naming conventions:
#[allow(non_camel_case_types)]
pub struct git_revspec {
...
}
Conditional compilation:
// Only include this module in the project if we're building for Android.
#[cfg(target_os = "android")]
mod mobile;
Suggest / force inlining:
//#[inline]
#[inline(always)]
fn do_osmosis(c1: &mut Cell, c2: &mut Cell) {
...
}
The #[foo]
attributes apply to a single
item.
In order to attach an attribute to a whole crate, use !#[foo]
at the top of main.rs or lib.rs.
For example, the #![feature]
attribute
is used to turn on unstable features of the Rust language and
libraries:
#![feature(trace_macros)]
fn main() {
// I wonder what actual Rust code this use of assert_eq!
// gets replaced with!
trace_macros!(true);
assert_eq!(10*10*10 + 9*9*9, 12*12*12 + 1*1*1);
trace_macros!(false);
}
Tests and Documentation
I already saw the test attribute for marking functions as tests:
#[test]
fn math_works() {
let x: i32 = 1;
assert!(x.is_positive());
assert_eq!(x + 1, 2);
}
These can then be run globally or individually with cargo test
or cargo test math
. The latter would run all fns
whose names contain "math"
Use something like this to test for a panicking fn:
#[test]
#[allow(unconditional_panic, unused_must_use)]
#[should_panic(expected="divide by zero")]
fn test_divide_by_zero_error() {
1 / 0; // should panic!
}
The allow attr is for convincing the compiler to let us do foolish things and not optimize them away.
It's useful to put tests into a separate module and have it only
compiled for testing. The #[cfg(test)]
attr checks for that:
#[cfg(test)] // include this module only when testing
mod tests {
fn roughly_equal(a: f64, b: f64) -> bool {
- b).abs() < 1e-6
(a }
#[test]
fn trig_works() {
use std::f64::consts::PI;
assert!(roughly_equal(PI.sin(), 0.0));
}
}
By default, cargo test
will run tests
multithreaded. To have tests run singlethreaded use cargo test -- --test-threads 1
Integration Tests
Integration tests live in tests/*.rs
files alongside the 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. The integration tests use
the SUT as an external dep.
Documentation
To generate docs run cargo doc
. To
generate docs for our project only and open them in a browser use cargo doc --no-deps --open
Doc comments for an item start with ///
Comments starting with //!
are treated
as #![doc] attributes and are attached to the enclosing feature, e.g. a
module or crate.
Doc comments can be Markdown-formatted. Markdown links can use Rust
item paths, like leaves::Leaf
to point to
specific items.
It's possible to include longer doc items:
#![doc = include_str!("../README.md")]
Doctests
Things that look like MD code blocks in docs will get compiled and run, for instance:
use std::ops::Range;
/// Return true if two ranges overlap.
///
/// assert_eq!(ranges::overlap(0..7, 3..10), true);
/// assert_eq!(ranges::overlap(1..5, 101..105), false);
///
/// If either range is empty, they don't count as overlapping.
///
/// assert_eq!(ranges::overlap(0..0, 0..10), false);
///
pub fn overlap(r1: Range<usize>, r2: Range<usize>) -> bool {
.start < r1.end && r2.start < r2.end &&
r1.start < r2.end && r2.start < r1.end
r1}
This generate two tests, one for each code block. Inside doctest code
blocks, use # my_test_setup()
to have
setup code hidden – the setup code will be run for doctests but not
displayed in the docs.
Doctests code blocks fenced with ```no_run
will be compiled but not actually
run
Specifying Dependencies
Ok, I already saw how to specify dependencies in Cargo.toml for crates hosted on crates.io, it's the default:
image = "0.6.1"
For a crate hosted on github:
image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }
A local crate:
image = { path = "vendor/image" }
Versions
Interestingly, the default version spec gives cargo some leeway in choosing crate versions, along the lines of semantic versioning where it chooses the latest version that should be compatible to the specified version. How well this works depends on how well the semantic versioning is aligned to reality I guess.
The version spec allows for more control too though, for example to
pin to an exact version: image = "=0.10.0"
Cargo.lock
However, if present cargo will consult a Cargo.lock file with exact
crate versions to use as dependencies. This is output when creating the
project and when running cargo update
. For
executables it's useful to commit Cargo.lock to source control for
repeatable builds. Static libraries on the other hand shouldn't bother
as their clients will have Cargo.locks of their own (shared libs
wouldn't though, commit Cargo.lock for those).
Publishing Crates to crates.io
As has become standard in the open source universe, Rust has built-in support for publishing crates to a central location, here crates.io (presumably with the same issues around trustworthiness of 3rd party packages/crates).
This is managed by cargo
as well,
specifically cargo package
to bundle up
sources. It uses data from the [package]
section of Cargo.toml to populate metdata like version info, license,
etc. Sensibly, if Cargo.toml specifies deps by path that conf is
ignored. For packages published to crates.io, it's deps should come from
there as well. It's also possible to specify dependencies by path and
have crates.io as a fallback.
The cargo login
command can get you an
API key, and cargo publish
can be used to
push the crate up.
Workspaces
To share dependencies it's possible to define a shared workspace for
related project via a Cargo.toml in a root dir with a [workspace]
section that lists the participating
subdirs (each containing a crate). This will download deps that are
common deps of those crates in a common subdir.
Also, the cargo build/test/doc
commands
accept a --workspace
flag; when specified
this will make the command act on all crates in the workspace.
More Nice Things
Publishing on crates.io pushes the crates docs to docs.rs
Githubs Travis CI has support Rust, and it looks like there's Actions as well
Generate a README.md file from top-level crate comment with the cargo-readme plugin
That's it for now!