Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Chạy được
cargo doc --openđể build và mở HTML documentation cho project Rust ở browser. - Phân biệt
///(outer doc comment — đặt trước item nó mô tả) và//!(inner doc comment — đặt bên trong module hoặc crate root). - Viết doc comment dùng markdown đầy đủ: heading, list, code block, link external, intra-doc link kiểu
[`Type`]tự resolve sang HTML anchor. - Viết doc example trong
```rust ... ```code block — và hiểu rằngcargo testsẽ thực sự compile và chạy các example đó như một dạng integration test. - Hiểu pipeline của docs.rs: mỗi version crate publish lên
crates.iotự được build documentation, host miễn phí, cross-link giữa các crate. - Dùng
cargo doc --no-depsđể chỉ build documentation cho crate hiện tại, bỏ qua transitive dep — tăng tốc đáng kể khi project lớn.
cargo doc --open Tạo HTML Local
Lệnh đơn giản nhất:
$ cargo doc --open
Cargo gọi rustdoc để parse toàn bộ source, extract doc comment, build HTML, ghi vào target/doc/, rồi mở trang root của crate ở browser mặc định. Output trông giống hệt docs.rs vì cả hai dùng cùng một engine (rustdoc).
Cấu trúc thư mục sau khi build:
target/doc/
├── <crate_name>/ # docs cho crate của bạn
│ ├── index.html
│ ├── struct.User.html
│ └── ...
├── serde/ # docs cho mỗi dependency
├── tokio/
├── settings.html
├── search-index.js
└── ...
Mặc định, cargo doc build cả documentation của tất cả dependency để bạn có thể click qua lại — search box global tìm symbol qua mọi crate. Với project nhỏ thì OK, với workspace lớn (50+ dep) thời gian build có thể vài phút. Mục 8 sẽ chỉ cách bỏ qua dep bằng --no-deps.
Một số flag hữu ích đi kèm:
--open— mở browser tự động sau khi build xong (giốngcargo run --openmentality).--document-private-items— include cả itemprivate(default chỉ public). Hữu ích khi xem internal API của chính crate mình.--workspace— build docs cho tất cả member trong workspace, không chỉ crate root.--release— chạy với profile release (chậm hơn nhưng cần khi dep yêu cầu build config khác).
/// Outer Doc Comment Cho Item
Comment bắt đầu bằng /// (ba dấu slash, khác với // comment thường) là outer doc comment — gắn vào item ngay sau nó: struct, enum, function, trait, const...
/// Đại diện một người dùng trong hệ thống.
///
/// `User` lưu thông tin cơ bản dùng cho authentication và hiển thị
/// trên UI. Tất cả field đều immutable sau khi tạo — muốn update
/// dùng `User::with_email` để clone với field mới.
pub struct Foo {
/// Tên hiển thị, không nhất thiết duy nhất.
pub name: String,
/// Email primary, dùng làm login key — phải duy nhất toàn hệ thống.
pub email: String,
/// Tuổi tính bằng năm tròn. Không lưu ngày sinh để tránh PII.
pub age: u32,
}
impl Foo {
/// Tạo `Foo` mới với tên và email; tuổi mặc định 0.
///
/// # Panics
///
/// Panic nếu `email` rỗng — kiểm tra bằng `email.is_empty()`
/// trước khi gọi nếu input đến từ user.
pub fn new(name: String, email: String) -> Self {
assert!(!email.is_empty(), "email không được rỗng");
Foo { name, email, age: 0 }
}
}
Điểm cần nhớ:
- Comment phải đặt ngay trước item — có dòng trống ở giữa cũng OK, nhưng không được có item khác chen vào.
- Field của struct được phép có doc comment riêng — rustdoc render thành bảng trên trang struct.
- Convention: dòng đầu là summary ngắn gọn (một câu), bỏ trống một dòng, rồi đến chi tiết. Summary hiển thị ở trang index và trong search result.
- Các section convention:
# Panics,# Errors,# Safety,# Examples— rustdoc render thành heading H1 trong trang item.
//! Inner Doc Comment Cho Module / Crate
Khác với /// gắn vào item phía sau, //! là inner doc comment — mô tả container chứa nó. Thường đặt ở đầu file src/lib.rs hoặc src/main.rs để làm crate-level documentation, hoặc đầu file module để làm module-level documentation:
//! # my_crate
//!
//! Thư viện helper cho việc parse CSV với schema động.
//!
//! ## Quick Start
//!
//! ```rust
//! use my_crate::Parser;
//!
//! let parser = Parser::new();
//! let rows = parser.parse("name,age\nAn,25").unwrap();
//! assert_eq!(rows.len(), 1);
//! ```
//!
//! ## Module
//!
//! - [`parser`] — engine chính, đọc và validate row.
//! - [`schema`] — định nghĩa kiểu dữ liệu cho từng cột.
//! - [`error`] — các loại error có thể trả về.
pub mod parser;
pub mod schema;
pub mod error;
Đoạn //! này sẽ hiển thị ở trang index.html root của crate — đây là trang đầu tiên user thấy khi vào docs.rs/my_crate. Khoản đầu tư viết tử tế ở đây có ROI cực cao: nó là "landing page" của crate.
Tương tự, đặt //! ở đầu file src/parser.rs sẽ thành documentation của module parser:
//! Engine parse CSV row-by-row.
//!
//! Module này cung cấp `Parser` — streaming parser không load toàn file
//! vào memory. Phù hợp cho CSV vài GB chạy trên máy 8GB RAM.
pub struct Parser { /* ... */ }
Tip: convention dùng //! ở đầu file là ngay dòng đầu tiên, không có comment khác phía trên. Tránh trộn lẫn // regular và //! ở đầu file vì gây nhầm với chuyện rustdoc có pick up hay không.
Markdown Trong Doc Comment
Rustdoc parse doc comment bằng CommonMark Markdown (qua thư viện pulldown-cmark). Mọi cú pháp Markdown chuẩn đều hoạt động:
/// # Heading H1
///
/// ## Heading H2
///
/// Đoạn văn thường, có **bold**, *italic*, `inline code`.
///
/// Danh sách:
/// - Item 1
/// - Item 2
/// - Nested item
///
/// Danh sách có thứ tự:
/// 1. Step một
/// 2. Step hai
///
/// Link external: [Rust Book](https://doc.rust-lang.org/book/).
///
/// Block quote:
/// > Đây là quote.
///
/// Code block:
///
/// ```rust
/// let x = 42;
/// ```
pub fn example() {}
Đặc biệt mạnh là intra-doc link: thay vì viết URL tay, dùng cú pháp [`TypeName`] hoặc [`module::function`] để rustdoc tự resolve sang HTML anchor đúng — kể cả khi rename item, link vẫn không vỡ:
/// Trả về list của [`User`] sắp xếp theo [`User::email`].
///
/// Internally gọi [`std::collections::BTreeMap`] để giữ order, khác với
/// [`HashMap`](std::collections::HashMap) không đảm bảo order.
///
/// Khi error xảy ra, trả về [`Result::Err`] với variant [`AppError::Db`].
pub fn list_users() -> Result<Vec<User>, AppError> {
todo!()
}
Rustdoc resolve [`User`] bằng cách lookup trong scope hiện tại — nếu User nằm trong crate hoặc đã use, link tự build. Path absolute kiểu [`std::collections::HashMap`] cũng OK. Khi rename User thành Account, compiler báo lỗi broken intra-doc link — không như link manual ./struct.User.html sẽ silent 404.
Bật warning intra-doc broken bằng thêm vào Cargo.toml:
[lints.rustdoc]
broken_intra_doc_links = "warn"
Doc Example Compile Như Test
Đây là một trong những đặc điểm killer của Rust documentation. Mỗi code block ```rust ... ``` trong doc comment được cargo test tự động extract, compile và chạy như một mini test riêng biệt:
/// Cộng hai số nguyên và trả về kết quả.
///
/// # Examples
///
/// ```rust
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// Với số âm:
///
/// ```rust
/// let result = my_crate::add(-1, 1);
/// assert_eq!(result, 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Chạy:
$ cargo test --doc
running 2 tests
test src/lib.rs - add (line 5) ... ok
test src/lib.rs - add (line 12) ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
Mỗi example được wrap trong một fn main() { ... } ngầm nếu không có, link với crate, compile riêng. Nếu API thay đổi (vd đổi add(a, b) sang add(&a, &b)), doc example sẽ fail compile ngay — bạn không thể accidentally ship documentation sai lệch với code.
Một số attribute điều chỉnh hành vi:
```rust,ignore— đoạn code chỉ để show, không compile (vd pseudo-code, code yêu cầu external service).```rust,no_run— compile nhưng không chạy (vd code mở file thật, gọi network).```rust,should_panic— phải panic mới pass test.```texthoặc```bash— block không phải Rust, rustdoc skip không test.- Dòng bắt đầu bằng
#trong code block bị ẩn khi render HTML nhưng vẫn compile — tiện để hide boilerplateuse:
/// ```rust
/// # use my_crate::Parser;
/// let parser = Parser::new();
/// assert!(parser.is_ready());
/// ```
User đọc HTML chỉ thấy 2 dòng cuối; cargo test --doc vẫn compile cả 3 dòng nên test passing. Best practice: example phải thực sự chạy được khi copy-paste — đó là contract giữa bạn với người dùng crate.
docs.rs Auto Build Cho Mọi Crate Publish
docs.rs là dịch vụ official của Rust Foundation: mọi crate publish lên crates.io đều được docs.rs tự pull về, chạy cargo doc trong sandboxed environment, host HTML output miễn phí ở URL https://docs.rs/<crate>/<version>. Không cần đăng ký, không cần cấu hình gì thêm — publish xong vài phút sau là có docs.
Pipeline cụ thể:
- Bạn chạy
cargo publish→ crate lêncrates.io. - docs.rs queue worker phát hiện crate mới (hook từ
crates.io). - Worker pull source, chạy
cargo doc --no-depsvới target tiêu chuẩn (Linux x86_64 mặc định, có thể cấu hình thêm target). - Output upload lên CDN, sẵn ở
docs.rs/<crate>/<version>. - URL
docs.rs/<crate>không có version sẽ redirect tới version mới nhất.
Tinh chỉnh build qua section [package.metadata.docs.rs] trong Cargo.toml:
[package]
name = "my_crate"
version = "0.1.0"
edition = "2024"
[package.metadata.docs.rs]
# Build với tất cả feature để docs đầy đủ
all-features = true
# Hoặc chỉ bật một số feature cụ thể
# features = ["async", "tls"]
# Build cho nhiều target (vd để show platform-specific API)
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
# Truyền rustdoc flag, vd bật unstable feature doc_cfg
rustdoc-args = ["--cfg", "docsrs"]
# Truyền cargo flag
cargo-args = ["-Z", "unstable-options"]
Section này chỉ tác động khi build trên docs.rs — local cargo doc không đọc. Combo phổ biến: bật all-features để mọi public API đều xuất hiện, dùng rustdoc-args = ["--cfg", "docsrs"] + #[cfg_attr(docsrs, doc(cfg(feature = "async")))] để hiển thị badge "Available on feature async only" trên từng item.
Một lợi ích cộng hưởng: intra-doc link giữa các crate vẫn hoạt động trên docs.rs. [`tokio::sync::Mutex`] trong doc của crate bạn sẽ link sang trang tokio trên docs.rs — toàn ecosystem được kết nối thành một cái wiki khổng lồ.
cargo doc --no-deps
Mặc định cargo doc build documentation cho cả dependency để cross-link. Với project lớn (workspace có 100+ transitive dep), bước này tốn vài phút và gigabyte disk. Khi chỉ quan tâm doc của chính crate mình, dùng:
$ cargo doc --no-deps --open
Documenting my_crate v0.1.0 (/Users/you/my_crate)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.2s
Opening /Users/you/my_crate/target/doc/my_crate/index.html
Output chỉ chứa target/doc/my_crate/, không có folder dep nào — build nhanh hơn 10-50 lần với workspace lớn. Trade-off: link tới type từ dep (vd tokio::sync::Mutex) sẽ là dead link local, nhưng khi publish lên docs.rs vẫn resolve được.
Một số combo hữu ích:
cargo doc --no-deps --document-private-items— build docs cả private item của riêng crate, không gen dep. Tốt cho team review internal API.cargo doc --no-deps --workspace— gen docs cho tất cả member của workspace nhưng skip external dep.cargo doc --no-deps --target wasm32-unknown-unknown— gen docs cho target khác, hữu ích khi crate có code#[cfg(target_arch = "wasm32")].
Tổng Kết
cargo doc --openbuild và mở HTML documentation từ doc comment trong source — dùng cùng enginerustdocvớidocs.rs.///outer doc comment mô tả item ngay sau nó (struct, fn, field...).//!inner doc comment mô tả container chứa nó, đặt ở đầu filelib.rs/main.rscho crate-level, hoặc đầu file module cho module-level.- Doc comment hỗ trợ full CommonMark Markdown: heading, list, code block, link external. Intra-doc link
[`Type`]tự resolve, không vỡ khi rename. - Code block
```rust ... ```đượccargo test --doccompile và chạy thật — example luôn đồng bộ với API. - Attribute điều chỉnh:
ignore,no_run,should_panic,text; dòng prefix#bị hide trong HTML nhưng vẫn compile. - docs.rs auto build mọi crate publish lên crates.io, cấu hình qua
[package.metadata.docs.rs]vớiall-features,targets,rustdoc-args. --no-depsbỏ qua build dep — nhanh hơn 10-50 lần với workspace lớn, trade-off là dead link tới external type khi xem local.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Khác biệt cốt lõi giữa
///và//!là gì? Đặt//!trước một struct thay vì đầu file thì rustdoc xử lý ra sao? - Bạn viết doc example trong code block
```rust ... ```. Chạycargo buildkhông báo lỗi, nhưngcargo test --docbáo lỗi compile. Vì sao kịch bản này có thể xảy ra? - Crate của bạn có feature
asyncbật mới có moduleasync_io. Trên docs.rs mặc định không thấy module này. SửaCargo.tomlthế nào để docs.rs build với feature đầy đủ? - So sánh viết
[Read more in docs](./struct.User.html)và[`User`]. Cái nào tốt hơn, vì sao? KhiUserđổi tên thànhAccount, hai cách handle khác nhau thế nào? - Workspace có 8 member crate.
cargo docmất 3 phút và disk usage 4GB. Hai cách để giảm build time/disk mà vẫn xem được docs của tất cả member? - Bạn viết example yêu cầu
use my_crate::Parser;vàuse std::fs::File;trước khi đến phần logic chính. Làm sao để user đọc HTML không thấy 2 dòngusenày nhưngcargo test --docvẫn compile đúng?
Đáp án
///= outer, mô tả item ngay sau nó (struct, fn).//!= inner, mô tả container chứa nó (crate root hoặc module). Đặt//!trước struct: rustdoc parse nhưng gán vào module/crate cha của struct đó (vì//!tham chiếu "container chứa nó"), không phải vào struct — có thể gây nhầm lẫn documentation. Compiler có warninginner doc comment in a position where it does not belongtrong một số trường hợp.- Code block
```rust ... ```đượccargo test --docwrap trongfn main()ngầm và compile riêng biệt với crate. Nếu example thiếuusestatement, dùng tên type không match (vd quên prefix crate name), hoặc dùng API đã thay đổi signature, compile sẽ fail.cargo buildkhông động đến doc comment nên không phát hiện. Best practice: chạycargo test --doctrong CI để bắt lỗi này sớm. - Thêm vào
Cargo.toml:[package.metadata.docs.rs]+all-features = true. Hoặc cụ thể hơnfeatures = ["async"]nếu chỉ muốn bật feature đó. Cộng thêmrustdoc-args = ["--cfg", "docsrs"]+#[cfg_attr(docsrs, doc(cfg(feature = "async")))]trên module để docs.rs hiển thị badge "Available on feature async only". [`User`]tốt hơn: rustdoc resolve sang URL HTML đúng tự động dựa trên symbol scope. KhiUserrename thànhAccount, intra-doc link báo lỗi compile (vớibroken_intra_doc_links = "warn"hoặc"deny"), bạn biết để sửa. Link manual./struct.User.htmlchỉ là string — rustdoc không biết kiểm tra, filestruct.User.htmlkhông còn tồn tại sau rename → silent 404, user click vào trang trắng.- (a) Dùng
cargo doc --no-deps --workspace— chỉ gen docs cho 8 member crate, bỏ qua external dep. Build nhanh hơn nhiều và disk chỉ chứa source của workspace. (b) Trade-off: link tới external type (vdtokio::Mutex) thành dead link local; nếu muốn hoạt động, deploy docs lên docs.rs hoặc internal host và cấu hình base URL. - Prefix dòng
usevới#trong code block:```rust+# use my_crate::Parser;+# use std::fs::File;+// logic chính ...+```. Khi render HTML, rustdoc ẩn các dòng#đi — user chỉ thấy logic. Khicargo test --doccompile, các dòng#vẫn được include nên test passing. Đây là pattern chuẩn cho doc example.
Bài Tiếp Theo
Bài 272: cargo expand — Xem Code Sau Macro Expansion — bài tiếp giới thiệu cargo expand, công cụ cargo subcommand (cài qua cargo install cargo-expand) để xem source code sau khi tất cả macro được expand — từ println!, #[derive(Debug)], đến custom proc-macro của bạn hay của thư viện như #[tokio::main]. Đây là cách debug khi macro generate code không như mong đợi, hoặc đơn giản để hiểu macro thực sự làm gì dưới hood. Tools series sẽ tiếp tục đi qua cargo bench, cargo audit, cargo deny, cargo flamegraph.
