rust-expert について
安全性、性能、保守性を重視したRust開発のスキルです。所有権、エラー処理、非同期コード、テスト、unsafe Rust、ビルド規約を扱います。
主な機能
- 01所有権と借用パターンの適用
- 02型付きコンテキストによるエラー処理
- 03Tokioの非同期処理と並行性の指針
- 04テストとプロファイリングの推奨
- 05unsafe Rust、FFI、Cargoの解説
活用シーン
- 01Rustコードの慣用パターンをレビュー
- 02エラー処理と非同期タスクの設計
- 03Rust crateのテスト計画
- 04unsafeコードとFFI境界の評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ oimiragieo/ agent-studio --skill rust-expertインストール後、Claude Code で/rust-expertを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
commands/rust-expert.mdhooks/post-execute.cjspre-execute.cjsreferences/research-requirements.mdrules/rust-expert.mdschemas/input.schema.jsonoutput.schema.jsonscripts/main.cjstemplates/implementation-template.md詳細
Rust エキスパート
安全性、パフォーマンス、保守性を高い水準で保証する、慣用的な Rust のパターンを適用します。
基本原則
- 型システムを通じて正しさをモデル化し、不正な状態を表現できないようにします。
- 暗黙的なものより明示的なものを優先し、すべてのシグネチャで所有権の意図を明確にします。
- 最小限でゼロコストの抽象化を記述し、目的のない割り当てや間接参照は避けます。
- 実装ではなく振る舞いをテストし、クレート境界での統合テストを優先します。
1. 所有権、借用、ライフタイム
所有権のパターン
// Prefer moves when value is consumed; prefer borrows when value is shared.
fn process(data: Vec<u8>) -> Vec<u8> { /* consumes */ }
fn inspect(data: &[u8]) -> usize { data.len() /* borrows */ }
// Clone only when necessary and explicitly documented.
// Anti-pattern: clone() to silence borrow checker errors — fix the design instead.
借用のルール(設計時に徹底)
- 可変参照は一度に1つだけ、または不変参照は任意の数だけ使用できます。
- 参照の有効期間は、参照先の値の有効期間を超えてはなりません。
- 割り当てを強制せずに借用または所有のセマンティクスが必要な場合は、
Cow<'a, T>を使用します。
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<str> {
if s.contains(' ') {
Cow::Owned(s.replace(' ', "_"))
} else {
Cow::Borrowed(s)
}
}
ライフタイムのパターン
// Explicit lifetime annotation: only when compiler cannot infer.
struct Parser<'input> {
source: &'input str,
pos: usize,
}
impl<'input> Parser<'input> {
fn next_token(&mut self) -> &'input str {
// Returns a slice of the original input — lifetime ties result to source.
&self.source[self.pos..]
}
}
// RPIT (Return Position Impl Trait) avoids lifetime noise in many cases.
fn words(s: &str) -> impl Iterator<Item = &str> {
s.split_whitespace()
}
避けるべきアンチパターン
- 借用チェッカーを回避するために、ホットループ内で
clone()を呼び出す。 - ライフタイムチェックを回避するために
unsafeを使う。代わりに設計を見直します。 - 借用で十分な場合に、ヒープ割り当てを強制する
'static境界。 - 構造体に
&mut Tを格納する。代わりに、所有データまたはRefCell<T>を優先します。
2. エラー処理
判断マトリックス
| シナリオ | ツール |
|---|---|
| ライブラリークレートのエラー | thiserror — 型付きで合成可能 |
| アプリケーション/バイナリーのエラー | anyhow — 人間工学的な ? の連鎖 |
| ドメイン固有のコンテキスト | thiserror によるカスタム列挙型 |
| 失敗しない変換 | From / Into — オーバーヘッドなし |
thiserror(ライブラリークレート)
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("missing required field: {field}")]
MissingField { field: &'static str },
#[error("invalid value for {field}: {source}")]
ParseError { field: &'static str, #[source] source: std::num::ParseIntError },
#[error(transparent)]
Io(#[from] std::io::Error),
}
anyhow(アプリケーション/バイナリー)
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading config from {path}"))?;
toml::from_str(&raw).context("parsing config TOML")
}
エラー伝播のルール
- ライブラリーコードで
.unwrap()を使わないでください。不変条件が自明なテストとmain()でのみ.expect()を使用します。 - 呼び出しチェーンを保持するため、すべてのエラー境界で
.context()/.with_context()を追加します。 - クレート境界でエラーをマッピングし、公開 API に内部エラー型を漏らさないでください。
3. トレイトシステム
ジェネリクスとトレイトオブジェクト
// Prefer generics (monomorphized, zero-cost) when types are known at compile time.
fn serialize<S: Serialize>(value: &S) -> Vec<u8> { /* ... */ }
// Use dyn Trait only when you need heterogeneous collections or runtime dispatch.
fn handlers() -> Vec<Box<dyn EventHandler>> { /* ... */ }
where 句
// Prefer where clauses for readability when bounds are complex.
fn merge<K, V>(a: HashMap<K, V>, b: HashMap<K, V>) -> HashMap<K, V>
where
K: Eq + Hash,
V: Clone,
{ /* ... */ }
ブランケット実装とオーファンルール
- 標準トレイト(
Display、From、Iterator)を独自の型に実装します。 - オーファンルールを守ってください。外部トレイトを実装できるのはローカル型に対してのみです。
- オーファンルールの制約を回避するには、ニュータイプパターンを使用します。
struct Wrapper(Vec<u8>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
Rust 1.75+ のトレイトにおける非同期処理(RPITIT)
// Stable as of Rust 1.75 — no more async-trait macro needed.
trait DataSource {
async fn fetch(&self, id: u64) -> anyhow::Result<Record>;
}
// Return-Position Impl Trait in Traits (RPITIT) — also stable in 1.75.
trait Transformer {
fn transform(&self, input: &str) -> impl Iterator<Item = String>;
}
4. Tokio による Async/Await
Tokio ランタイムのセットアップ
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Default: multi-thread runtime (all CPU cores).
Ok(())
}
// Single-thread runtime for I/O-only workloads.
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> { Ok(()) }
タスクの起動
use tokio::task;
// Spawn a non-blocking async task.
let handle = task::spawn(async move {
fetch_data(url).await
});
let result = handle.await?; // propagate JoinError
// CPU-bound work must go to the blocking thread pool — never block the async runtime.
let result = task::spawn_blocking(|| {
expensive_cpu_computation()
}).await?;
チャネル
use tokio::sync::{mpsc, oneshot, broadcast, watch};
// mpsc: producer → consumer pipeline.
let (tx, mut rx) = mpsc::channel::<Bytes>(1024);
// oneshot: request / response pattern.
let (resp_tx, resp_rx) = oneshot::channel::<Result<Record>>();
// broadcast: fan-out to multiple subscribers.
let (tx, _rx) = broadcast::channel::<Event>(16);
// watch: latest-value semantics (config reload, health state).
let (tx, rx) = watch::channel(Config::default());
select! とキャンセル
use tokio::select;
use tokio_util::sync::CancellationToken;
async fn worker(token: CancellationToken) {
select! {
_ = token.cancelled() => {
// Structured cancellation — always handle shutdown path.
}
result = do_work() => {
// Normal completion.
}
}
}
構造化並行性
use tokio::task::JoinSet;
async fn fetch_all(urls: Vec<String>) -> Vec<anyhow::Result<Bytes>> {
let mut set = JoinSet::new();
for url in urls {
set.spawn(fetch(url));
}
let mut results = Vec::new();
while let Some(res) = set.join_next().await {
results.push(res.expect("task panicked"));
}
results
}
非同期処理のアンチパターン
- 非同期コンテキスト内で
std::thread::sleepを使う。代わりにtokio::time::sleepを使用します。 .awaitをまたいでMutexGuardを保持する。代わりにtokio::sync::Mutexを使用します。- 非同期スレッドでブロッキング I/O(ファイル読み込み、DNS)を行う。
spawn_blockingまたはtokio::fsを使用します。 - 有界でないチャネルを使う。必ず容量の上限を設定します。
5. よく使われるクレート
serde — シリアライズ
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: u64,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}
axum — HTTP サーバー
use axum::{extract::{Path, State}, routing::get, Json, Router};
async fn get_user(
State(db): State<DbPool>,
Path(id): Path<u64>,
) -> Result<Json<User>, AppError> {
let user = db.find_user(id).await?;
Ok(Json(user))
}
let app = Router::new()
.route("/users/:id", get(get_user))
.with_state(db_pool);
sqlx — 非同期データベース
use sqlx::PgPool;
async fn insert_user(pool: &PgPool, name: &str) -> sqlx::Result<i64> {
let row = sqlx::query!("INSERT INTO users (name) VALUES ($1) RETURNING id", name)
.fetch_one(pool)
.await?;
Ok(row.id)
}
reqwest — HTTP クライアント
use reqwest::Client;
async fn fetch_json<T: for<'de> serde::Deserialize<'de>>(
client: &Client,
url: &str,
) -> anyhow::Result<T> {
Ok(client.get(url).send().await?.error_for_status()?.json().await?)
}
rayon — データ並列処理
use rayon::prelude::*;
fn parallel_sum(data: &[f64]) -> f64 {
data.par_iter().sum()
}
clap — CLI
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(short, long, env = "CONFIG_PATH")]
config: std::path::PathBuf,
}
6. パフォーマンスの最適化
ゼロコスト抽象化
- 手動のインデックスループよりイテレーターを優先します。同じ機械語にコンパイルされます。
- クレート境界を越える、ホットで小さな関数には
#[inline]を使用します。 - スタック割り当てを優先し、必要な場合にのみヒープ(
Box、Vec、Arc)へ移します。
SIMD
// Use std::simd (nightly) or portable-simd crate for explicit vectorization.
// Profile first — LLVM auto-vectorizes most iterator chains.
use std::simd::{f32x8, SimdFloat};
fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
a.chunks_exact(8)
.zip(b.chunks_exact(8))
.map(|(a_chunk, b_chunk)| {
let va = f32x8::from_slice(a_chunk);
let vb = f32x8::from_slice(b_chunk);
(va * vb).reduce_sum()
})
.sum()
}
プロファイリング
# flamegraph (install: cargo install flamegraph)
cargo flamegraph --bin my-app
# perf stat for CPU counters (Linux)
perf stat cargo run --release
# heaptrack for heap allocation analysis (Linux)
heaptrack cargo run --release
# criterion for micro-benchmarks
cargo bench
割り当てへの配慮
// Preallocate when size is known.
let mut v = Vec::with_capacity(expected_len);
// String building: use write! into a pre-allocated String.
use std::fmt::Write;
let mut s = String::with_capacity(256);
write!(s, "id={}", id)?;
// Avoid format!() in hot paths — prefer direct write!() or push_str().
7. テスト
ユニットテスト
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_config() {
let cfg = Config::from_str("[server]\nport = 9000").unwrap();
assert_eq!(cfg.port, 9000);
}
#[test]
fn parse_invalid_port_returns_error() {
let result = Config::from_str("[server]\nport = -1");
assert!(result.is_err());
}
}
統合テスト
// tests/integration/server.rs — tests the full public API.
#[tokio::test]
async fn health_endpoint_returns_200() {
let addr = spawn_test_server().await;
let resp = reqwest::get(format!("http://{addr}/health")).await.unwrap();
assert_eq!(resp.status(), 200);
}
プロパティベーステスト(proptest)
use proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(data: Vec<u8>) {
let encoded = encode(&data);
prop_assert_eq!(decode(&encoded).unwrap(), data);
}
}
非同期テスト
#[tokio::test]
async fn fetch_returns_data() {
let mock = MockServer::start().await;
Mock::given(method("GET")).respond_with(ResponseTemplate::new(200)).mount(&mock).await;
let result = fetch(&mock.uri()).await;
assert!(result.is_ok());
}
テストの規約
- テスト関数ごとに、1つの振る舞いをテストします。
- 説明的な名前を付けます:
<function>_<scenario>_<expected>。 - モックするのは外部 I/O の境界(HTTP、ファイルシステム、データベース)のみです。
- 開発中に診断情報を出力するには、
cargo test -- --nocaptureを実行します。 - CI でテストを並列実行し、速度を上げるには、
cargo nextest runを実行します。
8. Unsafe Rust
使用する場合
- FFI の境界(C 関数の呼び出し、C へのエクスポート)。
- 安全な抽象化では対応できない、低レベルのメモリマッピングや SIMD 組み込み関数。
- 安全な代替手段に、プロファイリングで実証された測定可能なオーバーヘッドがある、パフォーマンスが重要なコード。
ガイドライン
/// # Safety
///
/// - `ptr` must be non-null and properly aligned for `T`.
/// - The memory at `ptr` must be valid for `len` elements of type `T`.
/// - The caller must ensure no other mutable references to the memory exist.
pub unsafe fn from_raw_parts<T>(ptr: *const T, len: usize) -> &'static [T] {
std::slice::from_raw_parts(ptr, len)
}
- すべての
unsafeブロックには、その処理が健全である理由を説明する// SAFETY:コメントを付けます。 unsafeのスコープは最小限にし、すぐに安全な抽象化でラップします。- 関数全体が unsafe である場合は、安全な関数内の
unsafeブロックよりもunsafe関数を優先します。 - Miri(
cargo +nightly miri test)を使用して、unsafe コード内の未定義動作を検出します。
9. FFI と相互運用
C へのエクスポート
#[no_mangle]
pub extern "C" fn my_add(a: i32, b: i32) -> i32 {
a + b
}
Rust から C を呼び出す
extern "C" {
fn strlen(s: *const std::os::raw::c_char) -> usize;
}
fn rust_strlen(s: &str) -> usize {
let cstr = std::ffi::CString::new(s).expect("no null bytes");
// SAFETY: cstr is a valid, null-terminated C string.
unsafe { strlen(cstr.as_ptr()) }
}
cbindgen / bindgen
- Rust の公開 API から C ヘッダーを生成するには、
cbindgenを使用します。 - C ヘッダーから Rust バインディングを生成するには、
bindgenを使用します。 - API の乖離を検出するため、必ず CI で実行します。
10. ビルドシステム
Cargo ワークスペース
# Cargo.toml (workspace root)
[workspace]
members = ["crates/core", "crates/server", "crates/cli"]
resolver = "2"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
# crates/core/Cargo.toml
[dependencies]
tokio.workspace = true
serde.workspace = true
機能フラグ
[features]
default = ["std"]
std = []
async = ["dep:tokio"]
metrics = ["dep:prometheus"]
[dependencies]
tokio = { version = "1", optional = true }
prometheus = { version = "0.13", optional = true }
#[cfg(feature = "metrics")]
fn record_latency(ms: u64) { /* ... */ }
#[cfg(not(feature = "metrics"))]
fn record_latency(_ms: u64) {}
ビルドスクリプト
// build.rs — runs before crate compilation.
fn main() {
// Rerun when proto files change.
println!("cargo:rerun-if-changed=proto/");
// Link a system library.
println!("cargo:rustc-link-lib=ssl");
}
便利な Cargo コマンド
cargo build --release # optimized build
cargo clippy -- -D warnings # lint (treat warnings as errors)
cargo fmt --check # format check (CI)
cargo doc --no-deps --open # generate and open docs
cargo audit # check dependencies for CVEs
cargo deny check # license + advisory checks
cargo expand # show macro expansion
11. Rust 1.75 以降の機能
トレイト内の Async fn(1.75 で安定化)
// No longer requires #[async_trait] crate.
trait Fetcher {
async fn fetch(&self, url: &str) -> anyhow::Result<Bytes>;
}
RPITIT — トレイト内の戻り値位置 impl Trait(1.75 で安定化)
trait Source {
fn items(&self) -> impl Iterator<Item = &str>;
}
let-else(1.65 で安定化)
let Ok(value) = parse_value(raw) else {
return Err(ConfigError::InvalidValue);
};
std::io::ErrorKind — より詳細なバリアント(進行中)
use std::io::ErrorKind;
match err.kind() {
ErrorKind::NotFound => { /* ... */ }
ErrorKind::PermissionDenied => { /* ... */ }
_ => { /* ... */ }
}
12. コード品質チェックリスト
Rust の作業を完了とする前に、次の項目を確認します。
-
cargo clippy -- -D warningsが警告ゼロで成功する。 -
cargo fmt --checkによる変更がない。 -
cargo test(またはcargo nextest run)が成功し、すべてのテストが合格する。 - すべての
unsafeブロックに// SAFETY:コメントがある。 - 公開 API の項目に
///ドキュメントコメントがある。 - テスト以外のライブラリコードに
.unwrap()がない。 - エラー型が
Debugを導出し、std::error::Errorを実装している。 - 非同期コードがエグゼキューターのスレッドをブロックしない。
- パフォーマンスが重要な経路について、変更の前後でベンチマークを実施している。
割り当てられたエージェント
このスキルは、次のエージェントによって使用されます。
developer— Rust の機能実装とバグ修正code-reviewer— Rust 固有のコードレビューパターンqa— Rust のテスト戦略(proptest、criterion、nextest)
メモリプロトコル(必須)
開始前:
.claude/context/memory/learnings.md を読みます。
完了後:
- 新たな Rust パターンを発見した場合 ->
.claude/context/memory/learnings.md - Rust 固有の問題や注意点がある場合 ->
.claude/context/memory/issues.md - アーキテクチャやクレート選定の判断をした場合 ->
.claude/context/memory/decisions.md
中断を前提にしてください。コンテキストがリセットされることがあります。メモリに記録されていなければ、実行されていないものとみなされます。
コメント
Markdown 対応