處理依賴修補時,應遵循以下原則:

將修補視為最後手段

修補依賴應該是最後的選擇,因為隨著時間推移,修補很難維護。通常,我建議先嘗試以下方法:

  1. 檢查是否有較新版本解決了問題
  2. 考慮使用替代套件
  3. 評估自行實作所需功能的成本

在一個實際案例中,我曾經遇到一個加密函式庫能問題。雖然可以修補這個依賴,但經過評估後發現使用另一個更活躍維護的替代函式庫明智,這避免了長期維護修補的負擔。

向上遊提交修改

當修補確實必要時,應向開放原始碼專案提交修補,尤其是當許可證(如 GPL 許可的程式碼)要求時。這不僅有助於整個社群,還能減輕你維護修補的負擔。

我曾經為一個日誌函式庫了修補,解決了在特定平台上的問題。最初是為了自己的專案修補,但將修改提交上游後,不僅幫助了其他開發者,也讓我不必再維護自己的分支。

避免長期分支

避免分支上游套件,在無法避免的情況下,嘗試盡快回到主分支。長期分支會逐漸偏離原始程式碼,最終可能成為維護噩夢。

長期維護分支的成本往往被低估。當上游套件持續更新時,你的分支會逐漸積累差異,導致合併衝突和功能差異。這不僅增加了維護負擔,還可能引入難以追蹤的錯誤。因此,即使必須建立分支,也應視為臨時解決方案,並積極尋求迴歸主分支的機會。

套件發布流程與實務

對於想要發布到 crates.io 的專案,過程相對簡單。當套件準備好後,執行 cargo publish 命令,Cargo 會處理細節。發布套件有一些要求,例如指定許可證、提供專案檔案和倉函式庫RL 等專案詳情,並確保所有依賴也可在 crates.io 上取得。

雖然可以發布到私有登入檔,但在撰寫時,Cargo 對私有登入檔的支援相當有限。因此,對於私有套件,建議使用私有 Git 倉函式庫籤,而不依賴 crates.io。

CI/CD 整合:自動化發布流程

對於大多數套件,設定自動發布到 crates.io 的系統是很有價值的。持續整合/持續佈署(CI/CD)系統是現代開發週期的常見元件,通常由兩個不同步驟組成:

  1. 持續整合(CI):編譯、檢查和驗證每個提交到版本控制系統的變更
  2. 持續佈署(CD):在透過 CI 的必要檢查後,自動佈署每個提交或發布

以 GitHub Actions 為例,它對開放原始碼專案免費提供,可以輕鬆實作自動化發布流程。

典型的 Git 發布工作流程

當決定發布版本時,典型的 Git 工作流程如下:

  1. 如有需要,在 Cargo.toml 中更新版本屬性為要發布的版本
  2. CI 系統執行,驗證所有測試和檢查透過
  3. 為發布建立並推播標籤(使用版本字首,如 git tag -s vX.Y.Z
  4. CD 系統執行,構建標記的發布,並使用 cargo publish 發布到 crates.io
  5. 在新的提交中更新 Cargo.toml 中的版本屬性,為下一個發布週期做準備

這種工作流程將發布過程自動化,減少了人為錯誤,同時確保每個發布都經過了完整的測試和驗證。使用語義化版本控制(Semantic Versioning)並透過標籤明確標記發布,有助於使用者解版本變更的性質和影響。

在我的專案中,我特別重視自動化測試,確保每個發布在多個平台上都能正常工作。這減少了使用者告的問題,並提高了套件的整體品質。

實用技巧:Rustdoc 與特性標記

當使用 rustdoc 為專案生成檔案時,它會自動為你提供特性標記列表。這是 Rust 生態系統中檔案和程式碼密結合的一個很好例子。

使用 cargo doc --open 命令可以生成並開啟專案的檔案。對於有多個特性的套件,檔案會清楚顯示每個特性提供的功能,幫助使用者瞭解如何最佳化地使用你的套件。

結合實務經驗的建議

在處理 Rust 專案依賴時,我發現保持依賴樹的精簡和最新至關重要。定期審核依賴並移除未使用的套件可以顯著改善編譯時間和最終二進位檔的大小。

同時,對於關鍵依賴,值得投入時間瞭解其內部工作原理,這樣當問題出現時,你能更快找到解決方案。在某些情況下,將關鍵功能內部化(即複製到你的程式碼函式庫比維護一個分支更實用,特別是當該功能相對獨立與穩定時。

Rust 的套件生態系統雖然年輕但發展迅速,掌握 Cargo 的相依性管理發布功能可以使你的開發過程更加順暢,也能為整個社群做出貢獻。隨著專案規模的增長,良好的相依性管理踐將變得越來越重要,直接影響專案的長期可維護性和穩定性。

Rust 套件釋出與 C 函式庫實戰

在 Rust 生態系統中,套件(crate)釋出和與其他語言的互操作性是兩個關鍵的開發導向。這篇文章將探討如何有效地釋出 Rust 套件至 crates.io,以及如何透過外部函式介面(FFI)整合 C 語言函式庫些技術對於建立可重用的程式碼和擴充套件 Rust 應用功能至關重要。

套件釋出的不可變性原則

在 crates.io 釋出套件時,有一個基本原則必須謹記:

已釋出的套件是不可變的,任何變更都需要向前推進版本。一旦套件釋出到 crates.io,就無法回退或修改。

這種不可變性設計確保了相依性管理穩定性和可預測性。當其他開發者依賴你的套件時,他們可以確信特定版本的行為不會突然改變。這也意味著我們在釋出前必須格外謹慎,確保程式碼品質和功能完整性。

使用 GitHub Actions 自動化套件釋出流程

自動化釋出流程可以顯著提高效率並減少人為錯誤。讓我來分析 dryoc 套件如何使用 GitHub Actions 實作這一點。

建置與測試工作流程

首先,讓我們看建置和測試的工作流程設定:

name: Build & test
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
env:
  CARGO_TERM_COLOR: always
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
jobs:
  build:
    strategy:
      matrix:
        rust:
          - stable
          - beta
          - nightly
        features:
          - serde
          - base64
          - simd_backend
          - default
        os:
          - ubuntu-latest
          - macos-latest
          - windows-latest
        exclude:
          - rust: stable
            features: simd_backend
          - rust: beta
            features: simd_backend
          - os: windows-latest
            features: simd_backend

這個 GitHub Actions 設定檔案定義了一個複雜的測試矩陣,涵蓋了多種環境組合:

  1. 觸發條件:只有在 main 分支的推播和提取請求時才執行
  2. 矩陣策略
    • 測試三種 Rust 版本:穩定版、測試版和每夜版
    • 分別測試四種不同的功能特性:serde、base64、simd_backend 和預設
    • 在三種主要作業系統上測試:Linux、macOS 和 Windows
  3. 排除組合:某些功能(如 simd_backend)與特定 Rust 版本或平台不相容,因此被排除在測試矩陣外

這種多維度測試策略確保了套件在各種環境下的穩定性,大幅降低了釋出後發現相容性問題的風險。

接下來是實際的建置和測試步驟:

runs-on: ${{ matrix.os }}
env:
  FEATURES: >
    ${{ matrix.rust != 'nightly' && matrix.features
    || format('{0},nightly', matrix.features) }}
steps:
  - uses: actions/checkout@v3
  - name: Setup ${{ matrix.rust }} Rust toolchain with caching
    uses: brndnmtthws/rust-action@v1
    with:
      toolchain: ${{ matrix.rust }}
  - run: cargo build --features ${{ env.FEATURES }}
  - run: cargo test --features ${{ env.FEATURES }}
    env:
      RUST_BACKTRACE: 1
  - run: cargo fmt --all -- --check
    if: ${{ matrix.rust == 'nightly' && matrix.os == 'ubuntu-latest' }}
  - run: cargo clippy --features ${{ env.FEATURES }} -- -D warnings

這部分定義了實際的建置和測試步驟:

  1. 環境設定:根據測試矩陣動態設定要啟用的功能特性
  2. 工具鏈安裝:使用 brndnmtthws/rust-action 動作來安裝指定的 Rust 工具鏈
  3. 建置與測試:使用指定的功能特性執行建置和測試
  4. 程式碼格式檢查:僅在 nightly 版本的 Ubuntu 環境中執行 cargo fmt 檢查
  5. Clippy 檢查:使用 Clippy 靜態分析工具檢查程式碼,並將所有警告視為錯誤

這樣的工作流程不僅確保功能正確性,還強制執行了程式碼格式和品質標準。

釋出工作流程

釋出工作流程則更為簡單直接,專注於將套件釋出到 crates.io:

name: Publish to crates.io
on:
  push:
    tags:
      - v*
env:
  CARGO_TERM_COLOR: always
jobs:
  build-test-publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: brndnmtthws/rust-action@v1
        with:
          toolchain: stable
      - run: cargo build
      - run: cargo test
      - run: cargo login -- ${{ secrets.CRATES_IO_TOKEN }}
      - run: cargo publish
      - name: Create Release
        id: create_release
        uses: softprops/action-gh-release@v1
        if: startsWith(github.ref, 'refs/tags/')
        with:
          draft: false
math: true
          prerelease: false
          discussion_category_name: General
          generate_release_notes: true

這個工作流程專門處理套件釋出:

  1. 觸發條件:只有當推播標籤以 v 開頭時才執行(例如 v1.0.0
  2. 環境選擇:只在 Ubuntu 上執行,使用穩定版 Rust
  3. 釋出步驟
    • 執行基本的建置和測試,確保套件可以正常工作
    • 使用儲存在 GitHub Secrets 中的令牌登入 crates.io
    • 釋出套件到 crates.io
    • 在 GitHub 上建立一個對應的發布,包含自動生成的發布說明

值得注意的是,GitHub Actions 目前不支援在使用單獨階段時設定發布門檻(例如,等待建置階段成功後再進行發布階段)。為瞭解決這個問題,你必須確保建置階段成功後再推播任何標籤。

要使用這個工作流程,你需要:

  1. 在 crates.io 上建立一個帳戶
  2. 從 crates.io 帳戶設定中生成一個 API 令牌
  3. 將該令牌增加到 GitHub 儲存函式庫Secrets 設定中,名稱為 CRATES_IO_TOKEN

這樣的自動化釋出流程大簡化了套件維護工作,確保每次發布都經過了完整的測試和驗證。

連結 C 函式庫Rust 專案

在實際開發中,我們經常需要使用已有的 C 語言函式庫ust 透過外部函式介面(FFI)提供了與 C 語言程式碼互操作的能力。

使用 zlib 的簡單範例

讓我們以廣泛使用的壓縮函式庫lib 為例,實作簡單的壓縮和解壓縮功能。

首先,我們需要定義 C 函式的介面。以下是 zlib 中的三個關鍵函式:

int compress(void *dest, unsigned long *destLen, 
             const void *source, unsigned long sourceLen);
unsigned long compressBound(unsigned long sourceLen);
int uncompress(void *dest, unsigned long *destLen,
               const void *source, unsigned long sourceLen);

在 Rust 中,我們使用 extern "C" 來定義這些函式的介面:

use libc::{c_int, c_ulong};

#[link(name = "z")]
extern "C" {
    fn compress(
        dest: *mut u8,
        dest_len: *mut c_ulong,
        source: *const u8,
        source_len: c_ulong,
    ) -> c_int;
    
    fn compressBound(source_len: c_ulong) -> c_ulong;
    
    fn uncompress(
        dest: *mut u8,
        dest_len: *mut c_ulong,
        source: *const u8,
        source_len: c_ulong,
    ) -> c_int;
}

這段程式碼定義了 Rust 與 C 函式庫的介面:

  1. 引入 C 相容類別:使用 libc crate 提供的 C 相容類別,確保類別安全
  2. 連結指示#[link(name = "z")] 屬性告訴編譯器需要連結到 libz 函式庫當於加入 -lz 連結引數
  3. 外部函式宣告:使用 extern "C" 區塊宣告要使用的 C 函式,指定正確的引數類別和回傳值類別

當你編譯這個程式時,可以使用工具驗證連結是否成功:

  • macOS 上使用 otool -L
  • Linux 上使用 ldd
  • Windows 上使用 dumpbin

例如,在 macOS 上:

$ otool -L target/debug/zlib-wrapper
target/debug/zlib-wrapper:
  /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
  /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.60.1)
  /usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)

建立安全的 Rust 包裝函式

直接呼叫 C 函式在 Rust 中被視為不安全的操作,因此我們需要建立安全的 Rust 包裝函式。以下是壓縮函式的包裝:

pub fn zlib_compress(source: &[u8]) -> Vec<u8> {
    unsafe {
        let source_len = source.len() as c_ulong;
        let mut dest_len = compressBound(source_len);
        let mut dest = Vec::with_capacity(dest_len as usize);
        compress(
            dest.as_mut_ptr(),
            &mut dest_len,
            source.as_ptr(),
            source_len,
        );
        dest.set_len(dest_len as usize);
        dest
    }
}

這個函式提供了一個安全的介面來使用 zlib 的壓縮功能:

  1. 不安全區塊:所有 C 函式呼叫都包裝在 unsafe 區塊中
  2. 記憶體管理:使用 Rust 的 Vec 來管理記憶體,避免手動分配和釋放
  3. 計算空間需求:使用 compressBound 函式計算壓縮後資料的最大可能大小
  4. 呼叫 C 函式:傳遞適當的指標和長度引數給 C 函式
  5. 設定結果長度:根據實際壓縮後的長度設定 Vec 的長度
  6. 安全回傳:回傳包含壓縮資料的 Vec,讓呼叫者可以安全地使用結果

解壓縮函式的實作類別,但需要預先知道或估計解壓後的資料大小。

使用包裝函式

最後,讓我們看如何使用這些包裝函式:

fn main() {
    let hello_world = "Hello, world!".as_bytes();
    let hello_world_compressed = zlib_compress(&hello_world);
    let hello_world_uncompressed = zlib_uncompress(&hello_world_compressed, 100);
    
    assert_eq!(hello_world, hello_world_uncompressed);
    
    println!(
        "{}",
        String::from_utf8(hello_world_uncompressed)
            .expect("Invalid characters")
    );
}

這個簡單的範例展示了完整的壓縮和解壓縮流程:

  1. 準備輸入資料:將字串轉換為位元組切片
  2. 壓縮資料:使用我們的包裝函式壓縮資料
  3. 解壓縮資料:解壓縮資料,指定足夠的輸出緩衝區大小(這裡是 100

Rust 二進位釋出策略

在 Rust 開發生態中,二進位檔案的釋出是一個值得深入理解的主題。當我們將 Rust 應用程式封裝釋出時,需要考慮多種因素,包括平台相容性、連結策略和目標受眾。

二進位檔案的組成特性

Rust 編譯出的二進位檔案具有幾個獨特性:

  • 包含所有 Rust 依賴項,並整合為單一二進位檔案
  • 預設情況下,動態連結到 C 執行時函式庫 可選擇靜態連結到 C 執行時函式庫 平台依賴性強,不支援跨平台執行

在釋出 Rust 應用程式時,一個關鍵決策是否要靜態連結 C 執行時函式庫是依賴系統上已安裝的執行時。這個選擇會影回應用程式的可移植性和體積大小。

平台相容性的考量

Rust 編譯的二進位檔案具有嚴格的平台相依性。一個為 Intel x86-64 CPU 編譯的程式無法直接在 ARM 架構上執行,同樣,為 macOS 編譯的程式也無法在 Linux 上執行。

某些作業系統,特別是 Apple 的 macOS,提供了跨架構的模擬功能。例如,Apple 的 Rosetta 工具能夠自動讓 ARM 架構的 Mac 執行 x86-64 的應用程式。但這屬於例外情況,大多數時候,我們需要為每個目標平台單獨編譯。

與 Go 語言不同,Rust 預設需要 C 執行時函式庫與使用動態連結。這意味著在釋出時需要額外的考量,以確保最終使用者夠正確執行你的應用程式。

跨平台編譯技巧

跨平台編譯是 Rust 強大的功能之一,它允許我們在一個平台上編譯適用於其他平台的二進位檔案。然而,這個功能受限於編譯器對目標平台的支援。

可用目標平台探索

首先,我們可以使用 rustup 檢視當前環境支援哪些目標平台:

$ rustup target list
aarch64-apple-darwin
aarch64-apple-ios
aarch64-fuchsia
aarch64-linux-android
aarch64-pc-windows-msvc
...

上面的命令會列出所有可用的目標平台。每個目標平台都有一個特定的格式,通常包含 CPU 架構、廠商和作業系統訊息。例如,aarch64-apple-darwin 代表 ARM 64 位架構、Apple 硬體上執行的 Darwin 系統(即 macOS)。

安裝與使用目標平台

安裝新的目標平台非常簡單:

$ rustup target add aarch64-apple-darwin
info: downloading component 'rust-std' for 'aarch64-apple-darwin'
info: installing component 'rust-std' for 'aarch64-apple-darwin'
info: using up to 500.0 MiB of RAM to unpack components
18.3 MiB / 18.3 MiB (100 %) 14.7 MiB/s in 1s ETA: 0s

安裝完成後,我們可以使用 Cargo 為該目標平台編譯程式:

$ cargo build --target aarch64-apple-darwin
...
Finished dev [unoptimized + debuginfo] target(s) in 3.74s

這個過程分為兩步:首先使用 rustup target add 安裝目標平台的標準函式庫譯器支援;然後使用 cargo build --target 指定目標平台進行編譯完成後,二進位檔案會被放置在 target/<目標平台>/debug/ 目錄下。

不過,值得注意的是,雖然我們可以編譯出目標平台的二進位檔案,但如果當前系統不是該平台,則無法直接執行:

$ ./target/aarch64-apple-darwin/debug/simple-project
-bash: ./target/aarch64-apple-darwin/debug/simple-project: Bad CPU type in executable

在這個例子中,我在 Intel 處理器的 Mac 上編譯了 ARM 架構的程式,因此無法直接執行。若要執行,需要將二進位檔案複製到對應架構的裝置上。

靜態連結二進位檔案

C 執行時連結策略

標準的 Rust 二進位檔案包含所有編譯後的依賴項,但不包括 C 執行時函式庫 Windows 和 macOS 上,通常會分發預編譯的二進位檔案,並連結到作業系統的 C 執行時函式庫在 Linux 上,大多數套件是由發行版維護者從原始碼編譯的,發行版負責管理 C 執行時。

在 Linux 上釋出 Rust 二進位檔案時,可以使用 glibc 或 musl,取決於個人偏好:

  • glibc: 大多數 Linux 發行版的預設 C 函式庫時
  • musl: 更適合靜態連結,提供更好的可移植性

當我們希望釋出可在多種 Linux 環境下執行的二進位檔案時,我建議使用 musl 進行靜態連結。事實上,在某些目標平台上嘗試靜態連結時,Rust 會預設使用 musl。

設定靜態連結

我們可以透過 target-feature 標誌指示 rustc 使用靜態 C 執行時:

$ RUSTFLAGS="-C target-feature=+crt-static" cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.01s

這個命令透過 RUSTFLAGS 環境變數將 -C target-feature=+crt-static 引數傳遞給 rustc,指示編譯器使用靜態 C 執行時。Cargo 會解析這個環境變數並將其傳遞給 rustc

對於 x86-64 Linux 系統,我們可以結合 musl 目標和靜態連結:

$ rustup target add x86_64-unknown-linux-musl
...
$ RUSTFLAGS="-C target-feature=+crt-static" cargo build --target x86_64-unknown-linux-musl
...

如果想明確停用靜態連結,可以使用 RUSTFLAGS="-C target-feature=-crt-static"(將加號改為減號)。

使用設定檔設定連結策略

除了使用環境變數,我們也可以在 ~/.cargo/config 中指定 rustc 的編譯引數:

[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]

上述設定會在使用 x86_64-pc-windows-msvc 目標時,自動指示 rustc 進行靜態連結。這種方式比每次都設定環境變數更加方便,特別是當你需要為特定目標平台保持一致的連結策略時。

Rust 專案檔案生成

rustdoc 簡介

Rust 提供了一個強大的內建檔案工具 rustdoc,它與 Rust 一同釋出。如果你曾使用過其他程式語言的檔案工具(如 Javadoc、docstring 或 RDoc),那麼 rustdoc 的使用會讓你感到熟悉。

使用 rustdoc 非常簡單,只需在程式碼中增加註解,然後生成檔案即可。

快速入門範例

首先,讓我們建立一個示範用的函式庫:

$ cargo new rustdoc-example --lib
Created library `rustdoc-example` package

接下來,編輯 src/lib.rs 檔案,增加一個名為 mult 的函式,該函式接受兩個整數並回傳它們的乘積:

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

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn it_works() {
        assert_eq!(2 * 2, mult(2, 2));
    }
}

這段程式碼定義了一個公開函式 mult,它接受兩個 32 位整數引數並回傳它們的乘積。同時,我們還增加了一個測試模組,其中包含一個名為 it_works 的測試函式,用來驗證 mult 函式的正確性。

現在,我們還沒有增加任何檔案。讓我們先使用 Cargo 生成一些空的檔案:

$ cargo doc
Documenting rustdoc-example v0.1.0
(/Users/brenden/dev/code-like-a-pro-in-rust/code/c2/2.8/rustdoc-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.89s

生成的 HTML 檔案會被放在 target/doc 目錄下。你可以開啟 target/doc/src/rustdoc_example/lib.rs.html 檢視它們。初始的檔案是空的,但你可以看到公開函式 mult 已經被列在檔案中。

增加檔案註解

現在,讓我們為專案增加一些檔案。更新 src/lib.rs 檔案:

//! # rustdoc-example
//!
//! A simple project demonstrating the use of rustdoc with the function
//! [`mult`].

#![warn(missing_docs)]

/// Returns the product of `a` and `b`.
pub fn mult(a: i32, b: i32) -> i32 {
    a * b
}

這個更新版本的程式碼包含了幾個重要的檔案元素:

  1. //! 開頭的註解是 crate 級別的檔案字元串,它會出現在 crate 檔案的首頁上。
  2. #![warn(missing_docs)] 是一個編譯器屬性,它會在缺少公開函式、模組或類別的檔案時生成警告。
  3. /// 開頭的註解提供了 mult 函式的檔案。

Rust 檔案使用 CommonMark 格式,這是 Markdown 的一個子集。你可以在 https://commonmark.org/help 找到 CommonMark 的參考資料。

如果你重新執行 cargo doc 並在瀏覽器中開啟生成的檔案,就會看到更豐富的內容。

檔案釋出與 docs.rs

對於釋出到 crates.io 的 crate,有一個配套的 rustdoc 網站 https://docs.rs,它會自動為 crate 生成並託管檔案。例如,dryoc crate 的檔案可以在 https://docs.rs/dryoc 找到。

在有檔案的 crate 中,你應該更新 Cargo.toml 以包含 documentation 屬性,該屬性連結到專案的檔案。這對於那些在 crates.io 上找到你的 crate 的人很有幫助:

[package]
name = "dryoc"
documentation = "https://docs.rs/dryoc"

Cargo.toml 中增加 documentation 屬性,可以幫助使用者更容易找到你的專案檔案。docs.rs 網站會在新版本釋出到 crates.io 時自動生成更新的檔案,你不需要做任何額外的事情。

實用的檔案技巧

在開發 Rust 專案時,良好的檔案能夠顯著提高專案的可用性和可維護性。以下是我在寫檔案時發現的一些實用技巧:

  1. 使用 Markdown 格式化:充分利用 Markdown 的格式化功能,如標題、列表和程式碼,使檔案更易讀。

  2. 連結相關函式和類別:使用 [function_name] 語法可以自動建立到其他函式或類別的連結。

  3. 增加範例程式碼:在檔案中包含可執行的範例程式碼,幫助使用者解如何使用你的 API。