Cargo và crate trong Rust
Trong bài Bắt đầu, bạn đã dùng cargo new và cargo run. Đó mới chỉ là khởi đầu. Cargo là công cụ build và trình quản lý gói (package manager) của Rust. Nó biên dịch code, chạy test, định dạng file, và tải về các thư viện do người khác viết.
Những thư viện đó được gọi là crate. Hãy nghĩ về crate như ứng dụng trong một kho ứng dụng: ai đó đã giải quyết xong một vấn đề (đọc JSON, gửi request web, phân tích ngày tháng), và bạn có thể thêm giải pháp của họ vào dự án chỉ bằng một lệnh. Bài này cho bạn thấy cách làm điều đó, và cách chọn một crate tốt.
Crate và package
Phần tiêu đề “Crate và package”Có hai từ xuất hiện suốt:
- Crate là một đơn vị code Rust mà trình biên dịch build. Nó hoặc là binary (một chương trình chạy được, bắt đầu từ
src/main.rs) hoặc là library (thư viện, code để các chương trình khác dùng, bắt đầu từsrc/lib.rs). - Package là thứ mà
cargo newtạo ra: một thư mục chứa fileCargo.tomlvà một hoặc nhiều crate bên trong.
Khi nói chuyện hằng ngày, người ta gọi cả hai là “crate”. Khi ai đó nói “dùng crate serde”, ý họ là “thêm package serde từ crates.io vào dự án của bạn”.
Bên trong Cargo.toml
Phần tiêu đề “Bên trong Cargo.toml”Mỗi package có một file Cargo.toml. Nó được viết bằng TOML (tiếng Anh), một định dạng cấu hình đơn giản. Đây là thứ mà cargo new hello_rust tạo cho bạn:
[package]name = "hello_rust"version = "0.1.0"edition = "2024"
[dependencies][package]mô tả dự án của bạn.namevàversionlà những gì người khác sẽ thấy nếu bạn công bố nó.editionchọn phiên bản (edition) ngôn ngữ Rust để dùng. Các edition (2018, 2021, 2024) cho phép Rust cải tiến mà không làm hỏng code cũ. Cứ giữ nguyên những gìcargo newđã viết.[dependencies]liệt kê các crate mà code của bạn dùng. Ban đầu nó trống.
Khi dự án lớn dần, bạn có thể thấy thêm các mục khác:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
[dev-dependencies]# chỉ dùng cho test và ví dụ, không dùng trong chương trình thật của bạnpretty_assertions = "1.4"
[profile.release]# cấu hình cho `cargo build --release`opt-level = 3Thêm dependency bằng cargo add
Phần tiêu đề “Thêm dependency bằng cargo add”Bạn có thể tự sửa [dependencies] bằng tay, nhưng cách dễ nhất là cargo add. Nó tìm phiên bản mới nhất trên crates.io và viết dòng đó giúp bạn:
cargo add serde_jsonMột số crate có những phần tùy chọn gọi là feature. Chúng mặc định bị tắt để bản build của bạn gọn nhẹ. Bạn bật chúng bằng --features:
cargo add serde --features deriveSau hai lệnh này, Cargo.toml của bạn sẽ chứa đại loại như sau:
[dependencies]serde = { version = "1.0.228", features = ["derive"] }serde_json = "1.0.145"Giờ hãy dùng crate trong code. Lần cargo run tiếp theo sẽ tải nó về, biên dịch một lần, và liên kết nó vào chương trình của bạn:
use serde_json::json;
fn main() { let crab = json!({ "legs": 10, "name": "Ferris" });
println!("{}", crab); println!("Tên: {}", crab["name"]);}Kết quả:
{"legs":10,"name":"Ferris"}Tên: "Ferris"Có dấu ngoặc kép quanh "Ferris" vì crab["name"] vẫn là một giá trị JSON, chứ không phải một chuỗi Rust thông thường.
Rust Playground đã có sẵn những crate phổ biến nhất, nên đoạn này chạy được ở đó mà không cần cargo add. Trên máy của bạn, bạn cần làm bước cargo add serde_json trước. Bạn sẽ học nhiều hơn về crate này trong bài JSON với Serde.
Để gỡ một crate không còn cần nữa, hãy chạy cargo remove serde_json.
Yêu cầu phiên bản và Cargo.lock
Phần tiêu đề “Yêu cầu phiên bản và Cargo.lock”Khi bạn viết serde_json = "1.0", bạn không đòi đúng phiên bản 1.0. Bạn đang nói “bất kỳ phiên bản nào tương thích với 1.0”. Cargo tuân theo semantic versioning (SemVer, đánh số phiên bản theo ngữ nghĩa): một phiên bản có dạng MAJOR.MINOR.PATCH, và chỉ thay đổi ở số major mới được phép làm hỏng code của bạn.
| Bạn viết | Cargo có thể dùng | Vì sao |
|---|---|---|
"1.0" (giống "^1.0") |
>=1.0.0 và <2.0.0 |
các phiên bản 1.x đều phải tương thích với nhau |
"1.4.2" |
>=1.4.2 và <2.0.0 |
ít nhất là 1.4.2, nhưng vẫn là 1.x |
"0.4" |
>=0.4.0 và <0.5.0 |
trước 1.0, số minor được tính là thay đổi phá vỡ |
"~1.4" |
>=1.4.0 và <1.5.0 |
chỉ cập nhật bản vá (patch) |
"=1.4.2" |
đúng 1.4.2 |
hiếm khi cần |
Vậy ai quyết định phiên bản chính xác? Đó là việc của Cargo.lock. Lần build đầu tiên, Cargo chọn những phiên bản mới nhất khớp với yêu cầu của bạn và ghi chúng vào Cargo.lock. Mọi lần build sau đó đều dùng đúng những phiên bản ấy, trên máy bạn và trên máy của mọi người khác.
- Đừng sửa
Cargo.lockbằng tay. Cargo quản lý nó. - Commit nó vào git, đặc biệt với chương trình. Khi đó đồng đội và hệ thống CI của bạn sẽ build đúng như những gì bạn đã build.
cargo updatechuyển sang những phiên bản mới nhất vẫn khớp vớiCargo.toml, rồi ghi lạiCargo.lock. Nó không bao giờ nhảy lên một phiên bản major mới; muốn vậy bạn phải tự sửaCargo.toml.
Tìm một crate tốt
Phần tiêu đề “Tìm một crate tốt”Có ba trang web mà bạn sẽ dùng đi dùng lại:
- crates.io (tiếng Anh) là kho chính thức. Mọi crate công khai đều nằm ở đây. Hãy tìm một từ như “csv” hay “date”.
- docs.rs (tiếng Anh) lưu tài liệu của mọi crate, được build tự động. Ví dụ, docs.rs/serde_json (tiếng Anh).
- lib.rs (tiếng Anh) là một danh mục không chính thức, nhanh hơn, nhóm các crate theo chủ đề. Rất hợp để dạo xem.
Nhiều crate cùng giải quyết một vấn đề, vậy chọn thế nào? Trước khi thêm một crate, hãy dành một phút kiểm tra:
- Lượt tải. Crate phổ biến có hàng triệu lượt tải. Nghĩa là nhiều người đã tìm và sửa lỗi trước bạn.
- Cập nhật gần đây. Xem ngày phát hành bản mới nhất. Một crate không thay đổi nhiều năm vẫn có thể ổn (có những crate đơn giản là đã “xong”), nhưng hãy kiểm tra xem repository của nó có đầy những issue không ai trả lời không.
- Tài liệu. Mở trang docs.rs của nó. Crate tốt có trang đầu rõ ràng với một ví dụ bạn chép được.
- Phiên bản 1.0 trở lên. Không bắt buộc, nhưng nó cho biết tác giả cam kết một API ổn định.
- Ít dependency. Mỗi crate bạn thêm vào lại kéo theo các dependency của riêng nó. Càng nhỏ càng dễ tin tưởng.
Khi phân vân, hãy tìm “rust” cộng với vấn đề của bạn và xem crate nào được cộng đồng gợi ý đi gợi ý lại.
Các lệnh Cargo thiết yếu
Phần tiêu đề “Các lệnh Cargo thiết yếu”Bạn không cần học thuộc. Cứ để bảng này gần bên và bạn sẽ nhớ dần khi dùng:
| Lệnh | Tác dụng |
|---|---|
cargo new my_app |
Tạo một dự án binary mới (thêm --lib để tạo thư viện) |
cargo build |
Biên dịch vào target/debug/ |
cargo run |
Biên dịch và chạy (truyền đối số sau --: cargo run -- input.txt) |
cargo check |
Kiểm tra lỗi mà không tạo ra chương trình. Nhanh hơn build rất nhiều |
cargo test |
Chạy test của bạn (xem Kiểm thử) |
cargo fmt |
Định dạng toàn bộ code theo phong cách chuẩn |
cargo clippy |
Chạy linter. Nó gợi ý code đơn giản hơn, đúng chất Rust hơn |
cargo doc --open |
Build tài liệu HTML cho dự án của bạn và mọi dependency, rồi mở ra |
cargo update |
Cập nhật Cargo.lock lên những phiên bản tương thích mới nhất |
cargo build --release |
Biên dịch một chương trình đã tối ưu vào target/release/ |
Một thói quen tốt: chạy cargo check thường xuyên trong lúc viết, và cargo fmt cùng cargo clippy trước khi commit. cargo doc --open cũng là một viên ngọc ẩn. Nó cho bạn tài liệu đúng với phiên bản crate mà bạn đang dùng, và chạy được cả khi không có mạng.
Bản build debug và release
Phần tiêu đề “Bản build debug và release”Mặc định, Cargo tạo một bản build debug. Nó biên dịch nhanh và giữ thêm các phép kiểm tra an toàn, như panic khi số nguyên bị tràn. Rất hợp trong lúc bạn đang phát triển.
Bản build release (cargo build --release hoặc cargo run --release) biên dịch lâu hơn nhưng chạy nhanh hơn nhiều, thường gấp 10 lần trở lên. Hãy dùng nó khi phát hành chương trình hoặc khi đo tốc độ.
Code của bạn thậm chí còn biết được nó đang là loại build nào:
fn main() { if cfg!(debug_assertions) { println!("Bản build debug: các phép kiểm tra bổ sung đang bật"); } else { println!("Bản build release: tối ưu cho tốc độ"); }}Kết quả:
Bản build debug: các phép kiểm tra bổ sung đang bậtTrong Playground, bạn có thể chuyển từ “Debug” sang “Release” trong menu rồi chạy lại để thấy dòng còn lại.
Crate binary và library cùng nhau
Phần tiêu đề “Crate binary và library cùng nhau”Một package có thể chứa một library và một binary cùng lúc. Đây là cách bố trí rất phổ biến. Bạn đặt logic thật vào src/lib.rs, còn src/main.rs giữ gọn và chỉ việc gọi nó:
greeter/├── Cargo.toml└── src/ ├── lib.rs # crate library: logic dùng lại được └── main.rs # crate binary: chương trình bạn chạypub fn greet(name: &str) -> String { format!("Hello, {name}!")}fn main() { // thư viện được đặt tên theo package println!("{}", greeter::greet("Ferris"));}Vì sao tách ra như vậy? Code trong lib.rs dễ kiểm thử, và các chương trình khác (hoặc một binary thứ hai) có thể dùng lại nó. Playground chỉ có một file, nhưng bạn có thể có cảm giác tương tự với một pub mod, như trong bài Module:
mod greeter { pub fn greet(name: &str) -> String { format!("Hello, {name}!") }}
fn main() { println!("{}", greeter::greet("Ferris"));}Kết quả:
Hello, Ferris!Lời chào được giữ tiếng Anh để khớp với ví dụ src/lib.rs phía trên, vốn là code chạy trên máy của bạn chứ không chạy trong Playground.
Sơ lược về workspace
Phần tiêu đề “Sơ lược về workspace”Khi một dự án lớn lên thành nhiều package gắn bó với nhau (ví dụ, một ứng dụng dòng lệnh, một web server và một thư viện dùng chung), bạn có thể đặt chúng vào một workspace. Chúng dùng chung một Cargo.lock và một thư mục target/, nên mọi thứ được build cùng nhau và dùng cùng phiên bản dependency.
Cargo.toml ở cấp cao nhất liệt kê các thành viên:
[workspace]members = ["app", "core"]resolver = "3"my_project/├── Cargo.toml # file workspace ở trên├── Cargo.lock # dùng chung cho mọi thành viên├── app/ # một package binary└── core/ # một package libraryKhi đó cargo build ở cấp cao nhất sẽ build tất cả, và cargo run -p app chạy một thành viên. Bạn sẽ chưa cần điều này cho những dự án đầu tiên, nhưng nhận ra nó khi đọc code của người khác là điều tốt.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Dùng crate mà chưa thêm nó. Nếu bạn viết use serde_json::json; nhưng quên cargo add serde_json, trình biên dịch sẽ không tìm thấy nó:
error[E0432]: unresolved import `serde_json` --> src/main.rs:1:5 |1 | use serde_json::json; | ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json` | = help: you might be missing a crate named `serde_json`Rustc báo rằng không giải quyết được lệnh import serde_json và gợi ý có thể bạn đang thiếu crate tên serde_json. Cách sửa là chạy cargo add serde_json. Cũng hãy kiểm tra chính tả: tên crate dùng - hoặc _, và trong code thì - luôn trở thành _.
Quên một feature. Nếu #[derive(Serialize)] báo không tìm thấy derive macro, có lẽ bạn đã thêm serde mà không có --features derive. Hãy chạy lại cargo add serde --features derive; nó sẽ cập nhật dòng đã có.
Đo tốc độ bằng bản build debug. Nếu chương trình của bạn có vẻ chậm, hãy thử cargo run --release trước khi sửa bất kỳ dòng code nào.
Luyện tập
Phần tiêu đề “Luyện tập”1. Dùng một crate có sẵn trong Playground
Phần tiêu đề “1. Dùng một crate có sẵn trong Playground”Dùng macro json! từ serde_json để mô tả một crate với ba khóa: downloads (một con số), name và version. In toàn bộ giá trị, rồi chỉ in phiên bản.
Gợi ý: chép theo hình dạng của ví dụ crab trong phần Thêm dependency bằng cargo add. Trên máy của bạn, hãy chạy cargo add serde_json trước; Playground thì đã có sẵn.
Xem lời giải
use serde_json::json;
fn main() { let krate = json!({ "downloads": 500, "name": "serde_json", "version": "1.0" });
println!("{}", krate); println!("Phiên bản: {}", krate["version"]);}Kết quả:
{"downloads":500,"name":"serde_json","version":"1.0"}Phiên bản: "1.0"Biến được đặt tên là krate vì crate là từ khóa dành riêng trong Rust. Cách viết này là một câu đùa quen thuộc trong code Rust. Như trước, phiên bản được in kèm dấu ngoặc kép vì nó vẫn là một giá trị JSON; bài JSON với Serde chỉ cách lấy ra văn bản trần.
2. Thông báo chỉ có trong bản build debug
Phần tiêu đề “2. Thông báo chỉ có trong bản build debug”Viết hàm debug_log(msg: &str) in ra [debug] theo sau là thông báo, nhưng chỉ trong bản build debug. Dùng nó trong main quanh một phép tính nhỏ, và in kết quả như bình thường.
Gợi ý: bọc println! trong if cfg!(debug_assertions) { ... }, như trong phần Bản build debug và release.
Xem lời giải
fn debug_log(msg: &str) { if cfg!(debug_assertions) { println!("[debug] {msg}"); }}
fn main() { debug_log("bắt đầu tính toán"); let mut total = 0; for n in 1..=10 { total += n; } debug_log("tính toán xong");
println!("Tổng từ 1 đến 10 là {total}");}Kết quả:
[debug] bắt đầu tính toán[debug] tính toán xongTổng từ 1 đến 10 là 55Chuyển Playground sang “Release” (hoặc dùng cargo run --release trên máy) rồi chạy lại: chỉ có dòng cuối được in ra. Với dự án thật, bài Ghi log giới thiệu các crate làm việc này bài bản, với các cấp độ như debug và info.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Cargo build, chạy, kiểm thử và định dạng code của bạn, và nó tải crate từ crates.io.
Cargo.tomllà thứ bạn yêu cầu;Cargo.lockghi lại chính xác những phiên bản bạn nhận được. Hãy commit cả hai.cargo add name --features xthêm một dependency."1.0"nghĩa là “bất kỳ phiên bản 1.x tương thích nào”.- Đánh giá một crate qua lượt tải, cập nhật gần đây và tài liệu tốt trên docs.rs.
- Dùng
cargo checkkhi đang viết,cargo fmtvàcargo clippytrước khi commit, và--releasekhi cần tốc độ. - Đặt logic dùng lại được vào
src/lib.rsvà giữsrc/main.rsthật gọn.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Trong Cargo.toml, serde_json = "1.0" có nghĩa là gì?
2. File nào ghi lại chính xác phiên bản của mọi dependency mà bản build của bạn đã dùng?
3. Bạn muốn kiểm tra lỗi trong code nhanh nhất có thể, mà không cần tạo ra chương trình. Lệnh nào phù hợp nhất?
4. Làm sao thêm serde với feature derive được bật?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- The Cargo Book (tiếng Anh): hướng dẫn và tài liệu tham khảo chính thức cho mọi thứ Cargo làm.
- Specifying Dependencies — The Cargo Book (tiếng Anh): mọi cách viết yêu cầu phiên bản, cùng dependency từ git và từ đường dẫn.
cargo add— The Cargo Book (tiếng Anh): mọi tùy chọn củacargo add, như--dev,--featuresvà--no-default-features.- More about Cargo and Crates.io — The Rust Book (tiếng Anh): release profile, công bố crate của riêng bạn, và cài công cụ bằng
cargo install. - Cargo Workspaces — The Rust Book (tiếng Anh): một workspace được dựng từng bước, gồm một binary và hai thư viện.