Crates and cargo

Photo by Kelli McClintock on Unsplash This post is aimed at collecting some notes for crates usage, cargo is a fundamental element of this description being the main toolchain manager. It is more a roundup I needed to reconcile cargo file layout and default behavior with rust You can find other references about modules, crates and packages in the 7th chapter of the Rust Book. The code in this post is available on GitHub with MIT license.

a few things about cargo

basic default file layout for cargo

cargo has a default layout: with the same toml you may expect
  • a binary project as generated by
projectdir/
├─ Cargo.toml
└─ src/
   └─ main.rs
  • a library project
projectdir/
├─ Cargo.toml
└─ src/
   └─ lib.rs
in both case you get compilation with
cargo build
while in the binary case you can also run the code with
cargo run

compilation target basics

cargo has some commands and options aimed to compile differently the code and possibly also choose specific output When compiling the code with commands build or run the default target is dev while production compilation requires the flag cargo build --release : this triggers also some optimizations.

library and tests

library example generated by cargo new projectname --lib contains some test example to start unit testing: looks like this is the suggested feature
1: #[cfg(test)]
2: mod tests {
3:     #[test]
4:     fn it_works() {
5:         assert_eq!(2 + 2, 4);
6:     }
7: }
  • line 1 contains a macro that ensure the following entity to be compiled only if the "test" target is selected, which is what happens running cargo test
  • line 2 starts a module within this file, this will be compiled only according to the macro at line 1
  • line 3 contains a macro which will add some boilerplate code used to execute the following function and collect its result when executing tests
  • line 5 contains a macro checking the two arguments for equality and then raising an exception if the do not equals
    • equality is defined by a specific trait
    • if the exception is raised the test is failed
cargo test
  • this command also executes all the tests

how rust groups code

the code hierarchy

Rust uses this hierarchical structure
packages
└─ crates
   └─ modules
      ├─ functions
      ├─ structures and enums
      └─ traits
packages are the highest level: they can be external, built-in (e.g. std) or user created. Each project defined by a Cargo.toml is a package Each package can contain a hierarchy of crates, which means they have the same root and do not have circular dependencies Each crate can contain a hierarchy of modules with possibly different access permissions actual code is always in the modules

the crate and modules default

when creating a library project with cargo the lib.rs file contains the code which will be attributed by default to the crate with the same name as the project all modules are defined by using the mod keyword followed by a block; modules definition can be nested Modules act as namespace and as incapsulation protection. While modules on the same file are accessible, their inner objects should be marked as pub (short for public) to be accessible
mod mymodule {
    pub fn myfunc(){}
    pub mod mysubmodule{
        pub fn myotherfunc(){}
    }
}
accessing modules from within the same file can be done via a path
fn main(){
    mymodule::myfunc();
    mymodule::mysubmodule::myotherfunc();
}

use keyword and navigating the crate hierarchy

The rust book has a nice section dedicated to this subject. The main takeaways are:
  1. the use keyword let you access other modules in the hierarchy: levels are separated by a double colon ::
    use std::io;
    
  2. a module in the current project can refer to its ancestors using the super keyword at the beginning of the sequence
    use super::another::branch;
    
  3. the root of all modules in this project is referred with the keyword crate
    use crate::sublevel;
    
  4. one or more elements can be exposed out of their modules; possible clashes can be handled using aliases
    use std::io::Result;
    use std::io::{self, Write};
    use std::fmt::Result as FmtResult;
    

mixing all together

other compilation targets

files in the examples and tests are compiled according to the appropriate target
projectdir/
├─ Cargo.toml
├─ src/
│  └─ lib.rs
├─ examples/
└─ tests/
e.g. the following command
cargo build --example mycode
will compile the file examples/mycode.rs; to run it
cargo run --example mycode
the test command will build and execute also the tests under the tests directory. But now things starts to be tricky: how can you import the modules in the src directory in order to test them? the same applies for examples

accessing the default crate from test files

suppose we have the following function in our library
pub fn myfunc(){}
the crate name is taken from the Cargo.toml name attribute
use rust_blog;
#[test]
fn test_access_base(){
    rust_blog::myfunc();
}

accessing modules from different files

the following syntax, added to the lib.rs file is doing two actions:
  • the mod command search for a file (in this case in the current directory)
  • the pub modifier re-exports the command
pub mod poly;
  • this allows to add a test file like the following: note that this looks for a src/lib.rs which in turn redirects to src/poly.rs file
use rust_blog::poly;

mod test {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

marco.p.v.vezzoli

Self taught assembler programming at 11 on my C64 (1983). Never stopped since then -- always looking up for curious things in the software development, data science and AI. Linux and FOSS user since 1994. MSc in physics in 1996. Working in large semiconductor companies since 1997 (STM, Micron) developing analytics and full stack web infrastructures, microservices, ML solutions

You may also like...