個人的・開発時のベストプラクティス Rust 編

追加日時:    更新日時:   


clippy のお節介なルールも有効化する

lib.rs と main.rs の先頭に以下を書く。

#![deny(rust_2018_idioms)]
#![deny(clippy::all)]
#![deny(clippy::nursery)]

Cargo.toml のパッケージセクション

[package]
name = "foo"
version = "0.1.0"        # バージョン番号は 0.1.0 から始めることにする
edition = "2024"         # 最新が 2024 なので 2024 を指定する
resolver = "3"           # 最新が 3 なので 3 を指定する
rust-version = "1.90.0"  # その時の最新バージョンを指定することにしている
publish = false          # 誤って crates.io に公開してしまうのを防ぐ

# crates.io への公開する場合は以下のフィールドも書く
authors = ["hogehoge <hoge@example.com>"]
description = "..."
license = "MIT OR Apache-2.0"
readme = "README.md"   # workspace 構成の場合は ../README.md など
categories = [ ... ]   # categories は既定のものから選ぶ: https://crates.io/categories
keywords = [ ... ]     # keywords は自由だが、既存のものから選ぶと検索性が高まる: https://crates.io/keywords

Cargo.toml の dependencies セクション

パッチバージョンまですべてのバージョンを指定する。例:

[dependencies]
anyhow = "1.0.102"

Cargo.toml のバージョンの解釈は npm の pakcage.json 等とは異なり、正確にそのバージョンという意味ではなくそのバージョンより上のバージョンという意味である。しかも、anyhow = "1" のようにマイナーバージョン・パッチバージョンを省略した場合は、そのメジャーバージョンのすべてをサポートするという意味なので、anyhow v1.0.0 もサポートしているという意味になってしまう。

npm と比較すると、Cargo.toml における

anyhow = "1.0.102"

は、npm における

"anyhow": "^1.0.102"

と同じ意味であり、逆に

anyhow = "1"

"anyhow": "^1.0.0"

と同じ意味なのである。普通、"^1.0.0" のようなバージョン指定はしないと思うので [要出典]、Cargo.toml においても "1" のようなバージョン指定はしないほうがいいと思うのである。

リリースビルドでデバッグシンボルを残す

デバッグやプロファイリングをするときに元のシンボル名がわかるので便利。

[profile.release]
debug = true

Workspace 構成の場合はトップレベルの Cargo.toml に書けば良い。

workspace 構成

Workspace 側の Cargo.toml

[workspace]
resolver = "3"
members = [ ... ]

[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.90.0"
publish = false

[workspace.dependencies]
# すべての依存関係をここに書く
anyhow = "1.0.102"
chrono = { version = "0.4.44", features = ["serde"] }
# ...

[profile.release]
debug = true

Package 側の Cargo.toml

[package]
name = "foo"
version.workspace = true    # 基本的に .workspace = true でワークスペースの設定を継承する
edition.workspace = true
rust-version.workspace = true
publish = false

[dependencies]
anyhow.workspace = true
chrono.workspace = true

[dev-dependencies]
rstest.workspace = true

CLI のセットアップ

バイナリを開発する場合は、tracing のセットアップなど決まりきった作業を行う。

  • .env を読み込む
  • tracing のセットアップ

特に、tracing-subscriber はデフォルトで stderr ではなく stdout へ出力してしまうので、設定の変更が必要である (参考: Logging to stdout (instead of stderr) is a wrong default · Issue #2492 · tokio-rs/tracing)。

use std::path::Path;

use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Registry};

pub fn setup_cli() {
    let dotenv_result = dotenvy::dotenv();

    let layer = tracing_subscriber::fmt::Layer::default()
        .with_writer(std::io::stderr)
        .with_thread_ids(true)
        .with_filter(EnvFilter::from_default_env());

    Registry::default()
        .with(layer)
        .init();

    match dotenv_result {
        Ok(dotenv_path) => tracing::info!(path = %dotenv_path.display(), "loaded .env file"),
        Err(_) => tracing::warn!(".env file not found"),
    }
}
}

GitHub Actions でのチェック

以下を実行する。

env:
  RUSTFLAGS: -Dwarnings
  RUSTDOCFLAGS: -Dwarnings
  CARGO_TERM_COLOR: always
cargo build --all-targets --verbose
cargo build --all-targets --verbose --release
cargo clippy --all-targets --verbose
cargo test --verbose
cargo test --verbose --release
cargo doc --no-deps --document-private-items --verbose
cargo fmt --check

リポジトリのルートで実行する限り、--workspace オプションは不要である。--all オプションは古いので使わない。--all-target は build と clippy についてのみ必要である。デフォルトではバイナリ、test、example 等が対象にならない (多分)。

doc には --document-private-items を付ける。これにより、プライベートな項目についてもリンク切れなどのチェックが行われる。

cargo clippy が cargo check の機能を内包しているため、cargo check は実行しない。

tracing で format! のスタイルを使わない

値とともにログを出力したいとき、

tracing::info!("something happend! value={value}");

ではなく

tracing::info!(value, "something happend!");

の形で出力する。

前者だと出力されるものは 1 つの文字列になってしまうが、後者だと tracing が value というフィードが存在することを分かったうえでログ出力してくれるので便利である。例えば JSON 形式で出力するとき、value という名前のフィールドができる。