Danh sách bài viết

Bài 271: cargo doc — Generate API Documentation

Bài 271 của series Rust Cơ Bản — một trong những điểm khiến Rust nổi bật so với phần lớn ngôn ngữ khác là văn hoá documentation tích hợp sẵn: viết comment đặc biệt bằng /// hoặc //!, gõ một lệnh cargo doc --open, và bạn có một trang web HTML đầy đủ với navigation, search, cross-link, source view — không cần Sphinx, không cần JSDoc plugin, không cần build script riêng. Hơn thế nữa, mọi đoạn code Rust nằm trong doc comment đều được cargo test chạy thực sự để đảm bảo ví dụ không bị lệch với API. Khi bạn publish crate lên crates.io, dịch vụ docs.rs tự build documentation cho mọi version — người dùng vào docs.rs/your-crate là đọc được ngay, không cần bạn host. Bài này đi qua workflow viết doc comment đúng style, dùng markdown trong doc, viết runnable example, hiểu pipeline docs.rs, và dùng --no-deps để skip dependency khi build local.

10/06/2026
9 phút đọc
3 lượt xem
1

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ằng cargo test sẽ 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.io tự đượ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.
2

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ống cargo run --open mentality).
  • --document-private-items — include cả item private (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).
3

/// 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.
4

//! Inner Doc Comment Cho Module / Crate

Khác với /// gắn vào item phía sau, //!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.

5

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"
6

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.
  • ```text hoặ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 boilerplate use:
/// ```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.

7

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ể:

  1. Bạn chạy cargo publish → crate lên crates.io.
  2. docs.rs queue worker phát hiện crate mới (hook từ crates.io).
  3. Worker pull source, chạy cargo doc --no-deps với target tiêu chuẩn (Linux x86_64 mặc định, có thể cấu hình thêm target).
  4. Output upload lên CDN, sẵn ở docs.rs/<crate>/<version>.
  5. 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ồ.

8

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")].
9

Tổng Kết

  • cargo doc --open build và mở HTML documentation từ doc comment trong source — dùng cùng engine rustdoc với docs.rs.
  • /// outer doc comment mô tả item ngay sau nó (struct, fn, field...).
  • //! inner doc comment mô tả container chứa nó, đặt ở đầu file lib.rs/main.rs cho 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 ... ``` được cargo test --doc compile 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ới all-features, targets, rustdoc-args.
  • --no-deps bỏ 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.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Khác biệt cốt lõi giữa /////! là gì? Đặt //! trước một struct thay vì đầu file thì rustdoc xử lý ra sao?
  2. Bạn viết doc example trong code block ```rust ... ```. Chạy cargo build không báo lỗi, nhưng cargo test --doc báo lỗi compile. Vì sao kịch bản này có thể xảy ra?
  3. Crate của bạn có feature async bật mới có module async_io. Trên docs.rs mặc định không thấy module này. Sửa Cargo.toml thế nào để docs.rs build với feature đầy đủ?
  4. So sánh viết [Read more in docs](./struct.User.html)[`User`]. Cái nào tốt hơn, vì sao? Khi User đổi tên thành Account, hai cách handle khác nhau thế nào?
  5. Workspace có 8 member crate. cargo doc mấ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?
  6. Bạn viết example yêu cầu use my_crate::Parser;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òng use này nhưng cargo test --doc vẫn compile đúng?
Đáp án
  1. /// = 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ó warning inner doc comment in a position where it does not belong trong một số trường hợp.
  2. Code block ```rust ... ``` được cargo test --doc wrap trong fn main() ngầm và compile riêng biệt với crate. Nếu example thiếu use statement, 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 build không động đến doc comment nên không phát hiện. Best practice: chạy cargo test --doc trong CI để bắt lỗi này sớm.
  3. Thêm vào Cargo.toml: [package.metadata.docs.rs] + all-features = true. Hoặc cụ thể hơn features = ["async"] nếu chỉ muốn bật feature đó. Cộng thêm rustdoc-args = ["--cfg", "docsrs"] + #[cfg_attr(docsrs, doc(cfg(feature = "async")))] trên module để docs.rs hiển thị badge "Available on feature async only".
  4. [`User`] tốt hơn: rustdoc resolve sang URL HTML đúng tự động dựa trên symbol scope. Khi User rename thành Account, intra-doc link báo lỗi compile (với broken_intra_doc_links = "warn" hoặc "deny"), bạn biết để sửa. Link manual ./struct.User.html chỉ là string — rustdoc không biết kiểm tra, file struct.User.html không còn tồn tại sau rename → silent 404, user click vào trang trắng.
  5. (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 (vd tokio::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.
  6. Prefix dòng use vớ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. Khi cargo test --doc compile, các dòng # vẫn được include nên test passing. Đây là pattern chuẩn cho doc example.
11

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.