Home Wiki Programming & Logic Modules and Crates in Rust: Organizing a Large Industrial Project
Programming & Logic

Modules and Crates in Rust: Organizing a Large Industrial Project

The Module System: mod and Visibility

Modules let you group related code and control what is visible to the rest of your program. By default, everything inside a module is private.

mod sensors {
    // Private function -- only accessible inside this module
    fn calibrate(raw: f64) -> f64 {
        raw * 1.02 + 0.5
    }

    // Public function -- accessible from outside the module
    pub fn read_temperature() -> f64 {
        let raw = 71.3; // simulated raw ADC value
        calibrate(raw)   // can call private fn within the same module
    }

    pub fn read_pressure() -> f64 {
        2.15
    }
}

fn main() {
    // We can call pub functions through the module path
    let temp = sensors::read_temperature();
    println!("Temperature: {:.1} C", temp);

    // sensors::calibrate(50.0); // ERROR: calibrate is private
}

Use pub to expose functions, structs, and fields that other modules need. Keep internal details private to maintain clean boundaries.

File-Based Modules

As projects grow, you move modules into separate files. Rust supports two conventions:

Option A -- single file: Place code in src/sensors.rs and declare mod sensors; in src/main.rs.

Option B -- directory: Create src/sensors/mod.rs for the module root, then add sub-modules like src/sensors/temperature.rs.

src/
  main.rs            // mod sensors;
  sensors/
    mod.rs           // pub mod temperature; pub mod pressure;
    temperature.rs   // pub fn read() -> f64 { ... }
    pressure.rs      // pub fn read() -> f64 { ... }

In src/sensors/mod.rs:

pub mod temperature;
pub mod pressure;

In src/sensors/temperature.rs:

pub fn read() -> f64 {
    73.2 // simulated sensor reading
}

Both approaches are equivalent. The directory style works best when a module has several sub-modules.

use and Paths: Importing Items

The use keyword brings items into scope so you do not need to write the full path every time.

// Absolute path from crate root
use crate::sensors::temperature::read;

// You can rename imports to avoid collisions
use crate::sensors::temperature::read as read_temp;
use crate::sensors::pressure::read as read_press;

// Import multiple items from the same module
use std::collections::{HashMap, HashSet};

fn main() {
    let temp = read_temp();
    let press = read_press();
    println!("Temp: {} C, Pressure: {} bar", temp, press);
}

Path types: crate:: starts from the current crate root, super:: refers to the parent module, and self:: refers to the current module.

Crates: Libraries and Binaries

A crate is the smallest compilation unit in Rust. There are two kinds:

  • Binary crate -- has a main.rs with a fn main() entry point. Produces an executable.
  • Library crate -- has a lib.rs that exposes public items. Other crates depend on it.

A single package can contain both. For a factory monitoring system:

factory-monitor/
  Cargo.toml
  src/
    main.rs    // binary: runs the monitoring loop
    lib.rs     // library: shared types and logic
    models.rs  // pub struct Reading { ... }

In lib.rs:

pub mod models;

pub fn system_version() -> &'static str {
    "1.0.0"
}

In main.rs:

use factory_monitor::models::Reading;
use factory_monitor::system_version;

fn main() {
    println!("Factory Monitor v{}", system_version());
}

Cargo.toml: Dependencies and Features

Cargo.toml defines your crate metadata, dependencies, and optional features.

[package]
name = "factory-monitor"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"

[features]
default = ["logging"]
logging = []           # custom feature flag
advanced-diagnostics = ["dep:some-diagnostics-crate"]

In your code, use #[cfg(feature = "logging")] to conditionally compile blocks:

#[cfg(feature = "logging")]
pub fn log_reading(sensor: &str, value: f64) {
    println!("[LOG] {}: {}", sensor, value);
}

Run with features: cargo build --features advanced-diagnostics

Workspaces: Multi-Crate Projects

A workspace groups multiple related crates under one top-level Cargo.toml. All crates share a single target/ directory and lockfile. This is ideal for large industrial platforms.

# Root Cargo.toml
[workspace]
members = [
    "crates/sensors",
    "crates/alarms",
    "crates/dashboard",
]

Directory structure:

factory-platform/
  Cargo.toml              # workspace root
  crates/
    sensors/
      Cargo.toml           # [dependencies] can reference siblings
      src/lib.rs
    alarms/
      Cargo.toml
      src/lib.rs
    dashboard/
      Cargo.toml           # depends on sensors and alarms
      src/main.rs

In crates/dashboard/Cargo.toml:

[dependencies]
sensors = { path = "../sensors" }
alarms = { path = "../alarms" }

Build everything at once with cargo build from the workspace root, or target one crate with cargo run -p dashboard.

Practical Example: Structuring a Factory Monitor

Here is a complete project layout for a factory monitoring system:

factory-monitor/
  Cargo.toml                  # workspace
  crates/
    core/
      Cargo.toml
      src/lib.rs              # shared types: Reading, MachineId, AlarmLevel
    sensors/
      Cargo.toml              # depends on core
      src/lib.rs
      src/temperature.rs      # pub fn poll(id: &str) -> Reading
      src/pressure.rs
    alarms/
      Cargo.toml              # depends on core
      src/lib.rs
      src/rules.rs            # pub fn evaluate(r: &Reading) -> Option<Alarm>
    app/
      Cargo.toml              # depends on sensors, alarms
      src/main.rs             # entry point, monitoring loop

In crates/core/src/lib.rs:

pub struct Reading {
    pub machine_id: String,
    pub value: f64,
    pub unit: String,
}

pub enum AlarmLevel { Info, Warning, Critical }

In crates/app/src/main.rs:

use sensors::temperature;
use alarms::rules;

fn main() {
    let reading = temperature::poll("FURNACE-01");
    if let Some(alarm) = rules::evaluate(&reading) {
        println!("ALARM on {}: level {:?}", reading.machine_id, alarm);
    }
}

This structure keeps each concern isolated, testable, and reusable across different binaries.

Summary

  • Modules (mod) organize code into logical groups with controlled visibility via pub.
  • File-based modules scale projects by mapping modules to files and directories.
  • use imports items into scope, with support for renaming and grouping.
  • Crates are either binaries (main.rs) or libraries (lib.rs), and a package can contain both.
  • Cargo.toml manages dependencies, versions, and feature flags for conditional compilation.
  • Workspaces unify multi-crate projects under a single build, perfect for large industrial platforms.
  • A well-structured project separates core types, sensor logic, alarm rules, and the application entry point into distinct crates.
modules crates Cargo pub use workspace الوحدات الحزم التنظيم الرؤية المكتبات مساحة العمل