1. rust
  2. /basics
  3. /cargo-modules

Cargo & Modules

Cargo is Rust's build system and package manager, while modules provide a way to organize code within a project. Together, they form the foundation for building and structuring Rust applications.

Getting Started with Cargo

Cargo manages Rust projects, handles dependencies, builds code, runs tests, and more.

Creating a New Project

# Create a new binary project
cargo new my_project
cd my_project

# Create a new library project
cargo new my_library --lib

# Create a project in the current directory
cargo init

Project Structure

A typical Cargo project has this structure:

my_project/
├── Cargo.toml          # Package metadata and dependencies
├── Cargo.lock          # Exact versions of dependencies (auto-generated)
├── src/
│   ├── main.rs         # Main entry point for binary
│   └── lib.rs          # Library root (for --lib projects)
├── tests/              # Integration tests
├── examples/           # Example programs
├── benches/            # Benchmarks
└── target/             # Build artifacts (auto-generated)

Cargo.toml

The Cargo.toml file contains package metadata and configuration:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <[email protected]>"]
description = "A sample Rust project"
license = "MIT OR Apache-2.0"
repository = "https://github.com/username/my_project"
keywords = ["cli", "tool"]
categories = ["command-line-utilities"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
clap = "4.0"

[dev-dependencies]
proptest = "1.0"

[build-dependencies]
cc = "1.0"

Basic Cargo Commands

# Build the project
cargo build

# Build with optimizations (release mode)
cargo build --release

# Run the project
cargo run

# Run with arguments
cargo run -- --help

# Check if code compiles without building
cargo check

# Run tests
cargo test

# Generate documentation
cargo doc --open

# Format code
cargo fmt

# Lint code
cargo clippy

# Update dependencies
cargo update

# Clean build artifacts
cargo clean

Dependencies

Adding Dependencies

[dependencies]
# Latest compatible version
serde = "1.0"

# Exact version
rand = "=0.8.5"

# Version range
log = ">=0.4.0, <0.5.0"

# Git dependency
my_lib = { git = "https://github.com/user/my_lib.git" }

# Local path dependency
utils = { path = "../utils" }

# Optional dependency
database = { version = "1.0", optional = true }

# Dependency with specific features
tokio = { version = "1.0", features = ["full"] }

# Disable default features
reqwest = { version = "0.11", default-features = false, features = ["json"] }

Cargo.lock

The lock file ensures reproducible builds:

# Generate/update Cargo.lock
cargo build

# Use exact versions from Cargo.lock
cargo build --locked

# Update a specific dependency
cargo update -p serde

Modules

Modules help organize code into logical units and control visibility.

Basic Module Syntax

// src/lib.rs or src/main.rs
mod my_module {
    pub fn public_function() {
        println!("This is public");
    }
    
    fn private_function() {
        println!("This is private");
    }
    
    pub mod nested {
        pub fn nested_function() {
            println!("This is nested and public");
        }
    }
}

fn main() {
    my_module::public_function();
    // my_module::private_function(); // Error: private
    my_module::nested::nested_function();
}

Module Files

You can split modules into separate files:

src/main.rs:

mod math;      // Loads src/math.rs
mod utils;     // Loads src/utils.rs or src/utils/mod.rs

fn main() {
    let result = math::add(5, 3);
    println!("Result: {}", result);
    
    utils::helper::do_something();
}

src/math.rs:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

// Private function
fn internal_calculation() -> i32 {
    42
}

src/utils/mod.rs:

pub mod helper;    // Loads src/utils/helper.rs
pub mod config;    // Loads src/utils/config.rs

pub fn main_util_function() {
    println!("Main utility function");
}

src/utils/helper.rs:

pub fn do_something() {
    println!("Helper function doing something");
}

Visibility and Privacy

Control what's accessible outside modules:

mod outer {
    pub mod inner {
        pub fn public_function() {}
        
        pub(crate) fn crate_visible() {}        // Visible throughout the crate
        pub(super) fn parent_visible() {}       // Visible to parent module
        pub(in crate::outer) fn outer_visible() {} // Visible in outer module
        
        fn private_function() {}                // Only visible in this module
    }
    
    pub fn test_visibility() {
        inner::public_function();    // OK
        inner::crate_visible();      // OK
        inner::parent_visible();     // OK
        inner::outer_visible();      // OK
        // inner::private_function(); // Error: private
    }
}

Re-exports

Make items available through the current module:

mod math {
    pub fn add(a: i32, b: i32) -> i32 { a + b }
    pub fn subtract(a: i32, b: i32) -> i32 { a - b }
}

mod geometry {
    pub fn area_rectangle(w: f64, h: f64) -> f64 { w * h }
}

// Re-export everything from math
pub use math::*;

// Re-export specific items
pub use geometry::area_rectangle;

// Re-export with a different name
pub use math::add as sum;

Use Statements

Import items to avoid fully qualified names:

use std::collections::HashMap;
use std::io::{self, Write}; // Import io module and Write trait
use std::fmt::{Display, Debug}; // Import multiple traits

// Glob imports (use sparingly)
use std::collections::*;

// Aliasing
use very_long_module_name as short;

// Importing from current crate
use crate::math::add;
use self::local_module::function; // Relative to current module
use super::parent_function;       // From parent module

fn main() {
    let mut map = HashMap::new();
    map.insert("key", "value");
    
    let result = add(5, 3);
    writeln!(io::stdout(), "Result: {}", result).unwrap();
}

Library Structure

Creating a Library

cargo new my_library --lib

src/lib.rs:

//! # My Library
//! 
//! This library provides useful utilities for...

pub mod math;
pub mod string_utils;

// Re-export commonly used items
pub use math::{add, subtract};
pub use string_utils::capitalize;

/// A convenient function that combines multiple operations
pub fn process_data(input: &str, x: i32, y: i32) -> String {
    let result = add(x, y);
    capitalize(&format!("{}: {}", input, result))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_process_data() {
        assert_eq!(process_data("sum", 2, 3), "Sum: 5");
    }
}

src/math.rs:

/// Adds two numbers together
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Subtracts the second number from the first
pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_subtract() {
        assert_eq!(subtract(5, 3), 2);
    }
}

Binary with Library

You can have both a library and binary in the same crate:

my_project/
├── Cargo.toml
├── src/
│   ├── lib.rs          # Library code
│   ├── main.rs         # Binary entry point
│   └── bin/
│       ├── tool1.rs    # Additional binary
│       └── tool2.rs    # Another binary

src/main.rs:

use my_project::{add, process_data}; // Use library functions

fn main() {
    let result = add(5, 3);
    println!("Addition: {}", result);
    
    let processed = process_data("calculation", 10, 20);
    println!("Processed: {}", processed);
}

Workspaces

Workspaces allow managing multiple related packages:

Creating a Workspace

Cargo.toml (workspace root):

[workspace]
members = [
    "app",
    "lib",
    "utils",
]

[workspace.dependencies]
serde = "1.0"
tokio = "1.0"

Workspace Structure

my_workspace/
├── Cargo.toml          # Workspace configuration
├── Cargo.lock          # Shared lock file
├── target/             # Shared build directory
├── app/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── lib/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── utils/
    ├── Cargo.toml
    └── src/
        └── lib.rs

app/Cargo.toml:

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

[dependencies]
lib = { path = "../lib" }
utils = { path = "../utils" }
serde = { workspace = true }  # Use workspace version

Workspace Commands

# Build all packages in workspace
cargo build

# Build specific package
cargo build -p app

# Run binary from specific package
cargo run -p app

# Test all packages
cargo test

# Test specific package
cargo test -p lib

Features

Features allow conditional compilation and optional dependencies:

Defining Features

Cargo.toml:

[package]
name = "my_lib"
version = "0.1.0"

[features]
default = ["json"]          # Default features
json = ["serde_json"]       # Feature that enables serde_json
async = ["tokio"]           # Feature for async support
full = ["json", "async"]    # Meta-feature combining others

[dependencies]
serde_json = { version = "1.0", optional = true }
tokio = { version = "1.0", optional = true }

Using Features in Code

// Conditional compilation based on features
#[cfg(feature = "json")]
pub mod json_support {
    use serde_json;
    
    pub fn parse_json(input: &str) -> serde_json::Result<serde_json::Value> {
        serde_json::from_str(input)
    }
}

#[cfg(feature = "async")]
pub mod async_support {
    use tokio;
    
    pub async fn async_operation() -> String {
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        "Done".to_string()
    }
}

// Feature-gated function
#[cfg(all(feature = "json", feature = "async"))]
pub async fn async_json_processing(input: &str) -> Result<String, Box<dyn std::error::Error>> {
    let value = json_support::parse_json(input)?;
    let result = async_support::async_operation().await;
    Ok(format!("{}: {}", result, value))
}

Using Features

# Build with specific features
cargo build --features "json async"

# Build with all features
cargo build --all-features

# Build with no default features
cargo build --no-default-features

# Build with specific features only
cargo build --no-default-features --features "json"

Testing

Unit Tests

// src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_divide_success() {
        assert_eq!(divide(10.0, 2.0), Ok(5.0));
    }

    #[test]
    fn test_divide_by_zero() {
        assert_eq!(divide(10.0, 0.0), Err("Division by zero".to_string()));
    }

    #[test]
    #[should_panic(expected = "panic message")]
    fn test_panic() {
        panic!("panic message");
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // This test is ignored by default
        // Run with: cargo test -- --ignored
    }
}

Integration Tests

tests/integration_test.rs:

use my_library::{add, process_data};

#[test]
fn test_public_api() {
    assert_eq!(add(2, 3), 5);
}

#[test]
fn test_full_workflow() {
    let result = process_data("test", 1, 2);
    assert!(result.contains("3"));
}

Test Organization

tests/
├── common/
│   └── mod.rs          # Shared test utilities
├── integration_test.rs
└── api_test.rs

tests/common/mod.rs:

pub fn setup() -> TestEnvironment {
    // Common setup code
    TestEnvironment::new()
}

pub struct TestEnvironment {
    // Test state
}

impl TestEnvironment {
    pub fn new() -> Self {
        Self {}
    }
}

tests/integration_test.rs:

mod common;

use common::setup;

#[test]
fn test_with_setup() {
    let env = setup();
    // Test using shared setup
}

Examples

Creating Examples

examples/basic_usage.rs:

use my_library::{add, subtract};

fn main() {
    println!("Addition: {}", add(5, 3));
    println!("Subtraction: {}", subtract(8, 3));
}

examples/advanced_features.rs:

use my_library::process_data;

fn main() {
    let result = process_data("advanced", 10, 20);
    println!("Advanced processing: {}", result);
}

Running Examples

# Run specific example
cargo run --example basic_usage

# List all examples
cargo run --example

Publishing

Preparing for Publication

[package]
name = "my_awesome_crate"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <[email protected]>"]
description = "A very useful crate for doing things"
license = "MIT OR Apache-2.0"
repository = "https://github.com/username/my_awesome_crate"
documentation = "https://docs.rs/my_awesome_crate"
homepage = "https://github.com/username/my_awesome_crate"
keywords = ["utility", "helper", "awesome"]
categories = ["development-tools"]
readme = "README.md"
exclude = [
    "tests/fixtures/*",
    "benches/*",
]

Publishing Commands

# Check if package is ready for publishing
cargo publish --dry-run

# Publish to crates.io
cargo publish

# Login to crates.io
cargo login

# Yank a published version (make it unavailable for new projects)
cargo yank --vers 0.1.0

# Un-yank a version
cargo yank --vers 0.1.0 --undo

Best Practices

Project Organization

// Group related functionality
mod network {
    pub mod http;
    pub mod tcp;
    pub mod utils;
}

mod storage {
    pub mod database;
    pub mod cache;
    pub mod filesystem;
}

// Use clear, descriptive module names
mod user_management;  // Good
mod um;              // Bad

// Keep modules focused and cohesive
mod authentication {
    // Only authentication-related code
}

Dependency Management

[dependencies]
# Pin major versions for stability
serde = "1.0"          # Will use latest 1.x version
tokio = "1.20"         # Will use latest 1.20.x version

# Use specific features to reduce compile time and binary size
tokio = { version = "1.0", features = ["rt", "net"] }

# Group related dependencies
[dependencies]
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Async runtime
tokio = { version = "1.0", features = ["full"] }

Module Guidelines

  1. Keep modules focused: Each module should have a single responsibility
  2. Use clear hierarchies: Organize modules in a logical tree structure
  3. Control visibility: Only expose what needs to be public
  4. Document public APIs: Use doc comments for all public items
  5. Test comprehensively: Include both unit and integration tests

Cargo and modules form the backbone of Rust project organization. Cargo handles dependencies and build management, while modules provide code organization and visibility control. Together, they enable building maintainable and scalable Rust applications.