Saltar a contenido

11 - Crates and Cargo

What this session is

About 45 minutes. You'll learn how Rust code is organized - modules (folders/files), crates (one library or binary, like a "project"), and crates.io (the public package registry). You'll add a third-party dependency and use it.

Vocabulary

  • Package - what cargo new creates: a folder with Cargo.toml and one or more crates inside.
  • Crate - either a library (compiles to .rlib / .so) or a binary (compiles to an executable). A package usually has one crate.
  • Module - a unit of organization inside a crate. Maps to files and folders.

For most beginners, one package = one crate. We'll keep it that way here.

Single-file projects: organization with mod

For a single binary file, you can declare modules inline:

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

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

fn main() {
    println!("{}", math::add(2, 3));
    println!("{}", math::subtract(10, 4));
}

What's new: - mod math { ... } - declare a module called math. - pub fn - make the function public (visible outside the module). Without pub, it'd be private to the module. - math::add(2, 3) - call a function inside the module with ::.

Multi-file modules

Once a module gets big, move it to its own file:

src/
├── main.rs
└── math.rs

In main.rs:

mod math;       // declares that there's a module called math; Cargo finds it in math.rs

fn main() {
    println!("{}", math::add(2, 3));
}

In math.rs:

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

mod math; in main.rs is the declaration. Cargo looks for either math.rs or math/mod.rs. Either works.

For multi-level modules:

src/
├── main.rs
└── math/
    ├── mod.rs            (or src/math.rs with src/math/ for submodules)
    ├── basic.rs
    └── advanced.rs

In main.rs: mod math;. In math/mod.rs: pub mod basic; pub mod advanced;.

Library vs binary projects

If your project has src/main.rs, Cargo builds a binary. If it has src/lib.rs, Cargo builds a library. You can have both (a binary that uses the library) by having both files.

For a library you intend to publish or share, the convention is src/lib.rs with everything organized into modules under it.

use for imports

To avoid typing full paths everywhere, use use:

use std::collections::HashMap;

fn main() {
    let mut h = HashMap::new();      // shorter than std::collections::HashMap::new
    h.insert("alice", 30);
}

use std::collections::HashMap; brings HashMap into scope. You can use it unqualified within the file.

Multiple imports from the same path:

use std::collections::{HashMap, HashSet, BTreeMap};
use std::io::{self, Read, Write};        // self brings io itself in too

Glob (rarely used):

use std::collections::*;

use is conventional for everything outside the current file. For your own modules:

mod math;
use math::add;

fn main() {
    println!("{}", add(2, 3));
}

crates.io: the public registry

crates.io hosts the Rust community's open-source libraries. Hundreds of thousands of crates: HTTP clients, serializers, web frameworks, CLI parsers, database drivers, everything.

To use a crate, add it to Cargo.toml:

[dependencies]
serde = "1.0"
serde_json = "1.0"

Run cargo build (or cargo check) and Cargo downloads the crate + all its dependencies, builds them, and links them.

Convention: pin the major version. serde = "1.0" accepts 1.0.x and 1.y.z (semver-compatible).

A real example using serde and serde_json for JSON:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

The features = ["derive"] enables an optional feature of the crate (here, the derive macros for serde).

In src/main.rs:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let alice = Person { name: String::from("Alice"), age: 30 };
    let json = serde_json::to_string(&alice).unwrap();
    println!("{}", json);    // {"name":"Alice","age":30}

    let parsed: Person = serde_json::from_str(&json).unwrap();
    println!("{:?}", parsed);    // Person { name: "Alice", age: 30 }
}

#[derive(Serialize, Deserialize)] makes the struct JSON-convertible. Magic - but at compile time. No runtime reflection.

Run with cargo run.

Useful crates to know exist

For recognition only - you'll meet these in real code:

Crate What it does
serde, serde_json, serde_yaml Serialization
tokio Async runtime
reqwest HTTP client
axum, actix-web Web frameworks
clap Command-line argument parsing
anyhow, thiserror Error handling helpers
tracing Structured logging
criterion Benchmarking
rayon Easy parallelism
sqlx, diesel Database drivers/ORMs
rstest Parameterized tests

These are the "standard" crates almost every Rust project uses.

Cargo.toml fields

[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
authors = ["You <you@example.com>"]
description = "A short description"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/my-app"

[dependencies]
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
rstest = "0.23"

[dev-dependencies] - only used for tests, examples, benchmarks. Not in the final binary.

Cargo.lock

After cargo build, you'll see a Cargo.lock file. It records exact versions of every dependency (direct + transitive) used for the build.

  • For binary projects (apps): commit Cargo.lock. Ensures reproducible builds across machines.
  • For library projects (published crates): don't commit Cargo.lock. Downstream users will have their own.

Useful Cargo commands recap

Command What it does
cargo new --bin <name> New binary project.
cargo new --lib <name> New library project.
cargo add <crate> Add a dependency (modifies Cargo.toml).
cargo remove <crate> Remove a dependency.
cargo update Update dependencies (Cargo.lock).
cargo build / cargo build --release Compile.
cargo run Build and run.
cargo check Type-check without building.
cargo test Run tests.
cargo doc --open Generate and open docs.
cargo clippy Run the linter.
cargo fmt Auto-format.
cargo publish Publish to crates.io (needs an account).

Exercise

In your project:

  1. Add serde (with features = ["derive"]) and serde_json:

    cargo add serde --features derive
    cargo add serde_json
    
    (Or edit Cargo.toml directly.)

  2. Define a struct Book with title: String and author: String and year: u32. Derive Serialize, Deserialize, Debug.

  3. Create a Book, serialize to JSON, print it.

  4. Parse the JSON back, print the result.

  5. Now do it for a Vec<Book> of 3 books.

  6. Stretch: add clap (cargo add clap --features derive). Make a small CLI that takes a --name flag and prints Hello, <name>. Look up "clap derive" examples.

What you might wonder

"Why are there so many crates instead of a big standard library?" Philosophy. Rust's standard library is intentionally small. Most things (HTTP, async, serialization) live in third-party crates. Pro: stdlib stays small, evolves slowly, won't ship buggy features. Con: you have to pick crates for everything.

"How do I pick a crate?" Check crates.io for stars, downloads, last update. Check the README. Check open issues on GitHub. For well-known categories: serde (JSON), tokio (async), reqwest (HTTP), clap (CLI) are de facto standards.

"Why are some imports serde:: and some std::?" std is the standard library - always available. serde is a third-party crate you added. Cargo treats them the same syntactically.

"What's a workspace?" A multi-package project. The top-level Cargo.toml lists member packages (subdirectories), each with their own Cargo.toml. Common in big projects. Not needed for one-package learning.

Done

  • Organize code into modules with mod.
  • Use pub to control visibility.
  • Import with use.
  • Add dependencies from crates.io via Cargo.toml or cargo add.
  • Recognize the major crates of the Rust ecosystem.

Next: Reading other people's code →

Comments