πŸ“‚Project's Structure in Rust

An overview of how the Rust project is structured

πŸ’‘ Rust uses a specific folder structure to organize your project's code and resources. By using Cargo, we can generate a new package for Rust application.

Cargo

πŸ’‘ In Rust, Cargo is both the build system and the package manager.

Package Management:

  • Dependency Management: Cargo helps you declare your project's dependencies on other Rust crates (reusable libraries) using the Cargo.toml file.

  • Downloading Dependencies: When you build your project, Cargo automatically downloads the required crates and their dependencies from the Rust Package Registry (crates.io).

  • Resolving Conflicts: If dependencies have conflicting versions, Cargo attempts to find a compatible combination.

Build System:

  • Compiling Code: Cargo takes your Rust source code files (.rs) and compiles them into machine-readable code (usually an executable or library).

  • Linking Dependencies: It links your code with the downloaded dependencies to create a complete program or library.

  • Building Different Targets: Cargo allows building your project for different platforms or configurations by specifying targets.

Project Management:

  • Creating Projects: You can use cargo new to create a new Rust project with a basic directory structure.

  • Running Tests: Cargo can run unit tests written for your project using the cargo test command.

  • Building Documentation: Some crates support generating documentation using cargo doc.

Package Publishing:

  • Publishing Crates: If you've developed a reusable library, you can publish it to crates.io using cargo publish. This makes your crate available to other developers for use in their projects.

Common Cargo Commands

CommandDescription

rustup

A command-line tool for managing Rust versions and associated tools.

rustc

Rust compiler that helps to compile the Rust code to an executable binary file.

cargo new <package_name>

It creates a new Rust project.

cargo run

It compiles the code and then run the resultant executable all in one command.

cargo build

because the default build is debug build, hence, Cargo builds the Rust code and creates an executable file in directory target/debug/package_name.

cargo check

It quickly checks the code to make sure it compiles but doesn’t produce an executable file.

cargo build β€”release

It’s used for compiling the Rust code with optimizations. This command will create an executable in target/release. If you want to benchmarking your code’s running time, be sure to run this command and benchmark with executable in target/release.

cargo update

It’s used to update crates (dependencies).

Rust Package Structure

Basic Components

Cargo will help us to generate a new package. A package contains one or more crates that provide a set of functionality. A package allows you to build, test, and share crates.

Crate is a tree of modules that produces a library or executable. A library is a piece of code that you can share with other crates, while an executable is a program you can execute.

A crate that produces a library is called a library crate, and a crate that produces a executable is called a binary crate

The package rules in Rust:

  • A package must have at least 1 crate.

  • A package has at most 1 library crate.

  • A package has any number of binary crates as needed.

Generate Rust Package

To create a package in Rust, we’ll use Cargo.

In this example, we’ll create a hello_world package.

cargo new hello_world

After running the command, Cargo will generate a new package with the following structure

  • src: This directory will contain the source code of the package

    • main.rs: This is the entry point for all the code of the Rust project. It’s the starting point where execution begins when you run your Rust application.

    fn main() {
        println!("Hello, world!");
    }
  • target: This directory is a special location automatically generated by Cargo during the build process. It’s used to store the intermediate and final artifacts produced during the build process, and these artifacts can include compiled machine code (executables or libraries), debug information, and temporary files used by Cargo.

  • Cargo.toml: This is a configuration file that contains package descriptions and dependencies. It’s similar to the package.json file for the JavaScript project.

    • Everything that follows a header in this file is part of that section that continues until another section starts.

    • In [dependencies], you tell Cargo which external crates your project depends on and which versions of those crates you require.

    • Cargo understands Semantic Versioning (aka SemVer), which is a standard for writing version numbers.

      [package]
      name = "hello_world" # package name
      version = "0.1.0" # package version
      edition = "2021" # which Rust edition the package will be compiled with
      
      [dependencies]

Structure Package with Modules

A module is a way to organize and structure code within a package. It groups related functions, structs, enums, and constants together. It provides you the way to control the organization, scope, and privacy of your Rust package.

There are some benefits of modules:

  • Improve code readability, maintainability, and prevent naming conflicts.

  • Can be nested, creating a hierarchical structure for your codebase.

  • Control scope and privacy

  • Explicitly defined (using the mod keyword)

    • Not mapped to the file system

    • Flexibility and straightforward conditional complication

For example: We're going to structure a crate which is called my_utils. The my_utils crate will contain 2 modules: math_utils and string_utils modules.

Module structure
crate my_utils
β”œβ”€β”€ fn main: pub(crate)
β”œβ”€β”€ mod math_utils: pub(crate)
β”‚   └── mod math_utils: pub
β”‚       β”œβ”€β”€ fn add: pub
β”‚       └── fn subtract: pub
└── mod string_utils: pub(crate)
    └── mod string_utils: pub
        └── fn to_uppercase: pub
math_utils/mod.rs
pub mod math_utils {
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }
    pub fn subtract(x: i32, y: i32) -> i32 {
        x - y
    }
}
string_utils/mod.rs
pub mod string_utils {
    pub fn to_uppercase(text: &str) -> String {
        text.to_uppercase()
    }
}
lib.rs
mod math_utils;
mod string_utils;

use string_utils::string_utils::to_uppercase;
use math_utils::math_utils::{add, subtract};

fn main() {
    let adder = add(5, 3);
    println!("5 + 3 = {}", adder);

    let subtractor = subtract(5, 3);
    println!("5 - 3 = {}", subtractor);

    let uppercase_text = to_uppercase("hello, world!");
    println!("{}", uppercase_text);
}

In the main.rs, we first need to use the mod keyword to import math_utils and string_utils modules, and then use the use keyword to bring specific functions from the modules into the current scope.

Cargo Workspaces

πŸ’‘ A Cargo workspace in Rust is a powerful feature that allows you to manage multiple related Rust packages (crates) under a single directory.

Components of a Cargo Workspace

ComponentsDescription

Root Directory

This is the main directory that contains the Cargo.toml file for the workspace itself.

Member Packages

These are individual Rust packages (crates) within the workspace directory. Each member package has its own Cargo.toml file specifying its dependencies and configuration.

Shared Dependencies

All member packages within the workspace share a common dependency lock file (Cargo.lock) located in the workspace root. This ensures consistent dependencies across all packages.

Shared Target Directory

By default, all member packages compile their artifacts (libraries, executables) into a single target directory (target) within the workspace root. This reduces redundancy and simplifies managing build outputs.

workspace/
  Cargo.toml (workspace configuration)
  package1/  (member package directory)
    Cargo.toml (package1 specific configuration)
    src/ (source code for package1)
  package2/  (member package directory)
    Cargo.toml (package2 specific configuration)
    src/ (source code for package2)
  target/  (shared target directory for build outputs)

Benefits of Using Cargo Workspaces

  • Centralized Management: Workspaces provide a central location for managing multiple related projects, making development and maintenance more efficient.

  • Shared Dependencies: Efficient management of dependencies across all packages in the workspace ensures consistency and avoids version conflicts.

  • Simplified Testing: You can easily test how changes in one package affect other packages within the workspace by running tests across the entire workspace.

  • Simplified Commands: Common Cargo commands like build, test, and run can be executed for the entire workspace or specific member packages using workspace-specific flags.

Common Cargo Workspace Commands

Create Your First Workspace

Now, let’s explore how to establish a workspace and leverage its benefits for modular development.

Setting Up the Workspace

Begin by creating a new directory for your project, for example, my-utils. This directory will serve as the root of your workspace.

Navigate to the newly created directory and initialize it as a Cargo workspace using the following command. This command creates a Cargo.toml file at the root of your workspace, which will manage the overall project configuration.

cargo new my-utils --workspace

Within the Cargo.toml file, add a [workspace] section and specify the member crates for your project. In this example, we'll create three crates:

  • my-utils: This will be the binary crate responsible for the application logic.

  • math_utils: This library crate will house mathematical utility functions.

  • string_utils: This library crate will contain string manipulation utilities.

[workspace]
members = [
    "my-utils",
    "math_utils",
    "string_utils",
]

Use Cargo to create each member crate within the workspace:

cargo new my-utils            // Create the binary crate
cargo new math_utils --lib    // Create the math library crate
cargo new string_utils --lib  // Create the string library crate

Implementing Functionality in Member Crates

Now, let's add code to each crate to demonstrate their functionalities:

pub mod math_utils {
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }

    pub fn subtract(x: i32, y: i32) -> i32 {
        x - y
    }
}
pub mod string_utils {
    pub fn to_uppercase(text: &str) -> String {
        text.to_uppercase()
    }
}

Consuming Utilities in the Binary Crate

Next, we’re going to modify the Cargo.toml file of the my-utils crate to declare dependencies on the math_utils and string_utils crates:

[package]
name = "my_utils"
version = "0.1.0"
edition = "2021"

[dependencies]
math_utils = { path = "../math_utils" }
string_utils = { path = "../string_utils" }

By setting path in the dependencies, Cargo instructs Rust to look for the crates within the same workspace instead of external crates.

Finally, we’re going to update main.rs in the my-utils crate to import and utilize the functionalities from the other crates:

use math_utils::math_utils::{add, subtract};
use string_utils::string_utils::to_uppercase;

fn main() {
    let adder = add(5, 3);
    println!("5 + 3 = {}", adder);

    let subtractor = subtract(5, 3);
    println!("5 - 3 = {}", subtractor);

    let uppercase_text = to_uppercase("hello, world!");
    println!("{}", uppercase_text);
}

Now, running cargo run from the workspace root directory will execute the my-utils binary and demonstrate how it leverages the functionalities defined in the math_utils and string_utils crates.

Last updated