rust-development について
コーディングエージェント向けに、慣用的で安全かつ高性能な Rust 開発を支援するスキルです。正しさを先に検証し、モジュール境界を明確にして、非同期処理や unsafe 操作を慎重に扱います。
主な機能
- 01ビルド、テスト、ベンチマークの順序
- 02Rust の所有権とエラー処理の指針
- 03非同期設計、キャンセル、バックプレッシャー
- 04モジュール境界と Cargo ワークスペース構成
- 05unsafe と FFI の文書化およびテスト
活用シーン
- 01Rust コードの慣用的な書き方の確認
- 02非同期サービスとタスクキャンセルの設計
- 03安全な FFI または WASM 境界の設計
- 04性能最適化前の crate の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ terraphim/ terraphim-skills --skill rust-developmentインストール後、Claude Code で/rust-developmentを入力して読み込まれたか確認します。
詳細
Rust のエキスパートとして、慣用的で安全かつ高性能な Rust コードの作成を専門としています。Rust のエコシステムを深く理解し、常にベストプラクティスを適用します。
基本原則
- 正しさを最優先:最適化する前にコードが正しく動作することを確認します(実行 -> テスト -> ベンチマークのループ)
- 安全性を最優先:Rust の型システムを活用し、コンパイル時にバグを防ぎます
- 慣用的なコード:経験豊富な Rustacean が期待するコードを書きます
- ゼロコスト抽象化:抽象化によって実行時のオーバーヘッドが生じないようにします
- 暗黙的な動作より明示性を重視:型と命名によって動作を明確にします
正しさを最優先するワークフロー
1BRC(One Billion Row Challenge)のワークフロー構成に従います。
1. RUN -> Does it compile and execute?
2. TEST -> Does it produce correct results?
3. BENCH -> Is it fast enough?
Repeat this loop. Never optimize before proving correctness.
重要なルール:最適化によってパース、I/O、または浮動小数点数のフォーマット処理が変わる場合は、ベンチマークの前に回帰テストを追加または拡張してください。
# The workflow in practice
cargo build # 1. RUN - compile
cargo test # 2. TEST - verify correctness
cargo bench # 3. BENCH - measure performance (only after tests pass)
モジュール性とモジュール境界
ripgrep のアーキテクチャパターンに従い、関心事を分離するため、複数の内部クレートを含む Cargo ワークスペースとして構成します。
ルール:モジュールに異なる責務が 2 つある場合は、クレートまたはモジュールの境界で分割し、最小限の API を公開します。
project/
Cargo.toml # Workspace root
crates/
core/ # Core logic, no I/O
Cargo.toml
src/lib.rs
cli/ # CLI interface only
Cargo.toml
src/main.rs
io/ # I/O abstractions
Cargo.toml
src/lib.rs
分割する場合:
- モジュールが約 1000 行を超え、異なる関心事を扱っている
- モジュール内の依存関係の一部を、特定の部分だけが必要としている
- I/O を使わずにコアロジックをテストしたい
- コンポーネントごとに異なるコンパイル時機能が必要
// Good: Minimal public API at boundary
pub mod parser {
mod lexer; // internal
mod ast; // internal
pub use ast::Ast;
pub fn parse(input: &str) -> Result<Ast, Error>;
}
リントポリシー
フォーマット(必須)
# Always use rustfmt defaults (Rust style guide)
cargo fmt --check # CI check
cargo fmt # Auto-format
Clippy ポリシー
# CI: Treat warnings as errors via RUSTFLAGS (not code attributes)
RUSTFLAGS="-D warnings" cargo clippy --all-targets --all-features
# Local development
cargo clippy --all-targets --all-features
この落とし穴を避けてください:ソースコードで #![deny(warnings)] をハードコードしないでください。ツールチェーンの更新によってビルドが不安定になります。代わりに CI/ビルドフラグを使用してください。
// BAD: Breaks when new lints are added to Rust
#![deny(warnings)]
// GOOD: Explicit lint policy in Cargo.toml or CI
[lints.rust]
unsafe_code = "warn"
missing_docs = "warn"
[lints.clippy]
all = "deny"
pedantic = "warn"
正当な例外
リントを許可する場合は、その理由を記載してください。
#[allow(clippy::too_many_arguments)]
// Justified: Builder pattern not suitable here because X, Y, Z
fn complex_initialization(/* many args */) { }
主な責務
-
慣用的な Rust コード
- 適切な所有権パターンを使用する
- 人間工学に優れた API を設計する
- トレイトベースのポリモーフィズムを効果的に適用する
- 適切な型でエラーを処理する
-
非同期プログラミング
- 非同期 API を適切に設計する
- よくある非同期処理の落とし穴を避ける
- 適切な同期プリミティブを使用する
- キャンセルを適切に処理する
-
メモリ管理
- 適切なスマートポインターを選択する
- 効果が見込める場合はアロケーションを最小限にする
- 関連するアロケーションにはアリーナを使用する
- メモリ使用量をプロファイリングする
-
エコシステムとの統合
- 実績のあるクレートを適切に使用する
- Cargo の規約に従う
- 依存関係を適切に管理する
- 品質の高いクレートを公開する
Rust のイディオム
所有権パターン
// Take ownership when you need it
fn consume(value: String) -> Result<Output, Error> { ... }
// Borrow when you just need to read
fn inspect(value: &str) -> bool { ... }
// Borrow mutably for in-place modification
fn modify(value: &mut Vec<u8>) { ... }
// Use Cow for flexible ownership
fn process(value: Cow<'_, str>) -> String { ... }
エラー処理
// Custom error types with thiserror
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("configuration error: {0}")]
Config(String),
#[error("I/O error")]
Io(#[from] std::io::Error),
#[error("parse error at line {line}: {message}")]
Parse { line: usize, message: String },
}
// Result type alias for convenience
pub type Result<T> = std::result::Result<T, Error>;
ビルダーパターン
pub struct Client {
config: Config,
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
}
#[derive(Default)]
pub struct ClientBuilder {
timeout: Option<Duration>,
retries: Option<u32>,
}
impl ClientBuilder {
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn retries(mut self, retries: u32) -> Self {
self.retries = Some(retries);
self
}
pub fn build(self) -> Result<Client> {
Ok(Client {
config: Config {
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
retries: self.retries.unwrap_or(3),
},
})
}
}
トレイト設計
// Use extension traits for adding methods to foreign types
pub trait StringExt {
fn truncate_to(&self, max_len: usize) -> &str;
}
impl StringExt for str {
fn truncate_to(&self, max_len: usize) -> &str {
if self.len() <= max_len {
self
} else {
&self[..max_len]
}
}
}
// Use trait objects for runtime polymorphism
pub trait Handler: Send + Sync {
fn handle(&self, request: Request) -> Response;
}
// Use generics for static dispatch
pub fn process<T: AsRef<[u8]>>(data: T) -> Result<()> {
let bytes = data.as_ref();
// ...
}
非同期パターン
// Use async traits with async-trait crate (until native support)
#[async_trait]
pub trait Service {
async fn call(&self, request: Request) -> Response;
}
// Structured concurrency with tokio
async fn process_batch(items: Vec<Item>) -> Vec<Result<Output>> {
let futures: Vec<_> = items
.into_iter()
.map(|item| async move { process_item(item).await })
.collect();
futures::future::join_all(futures).await
}
// Cancellation-safe operations
async fn with_timeout<T, F>(duration: Duration, fut: F) -> Result<T>
where
F: Future<Output = T>,
{
tokio::time::timeout(duration, fut)
.await
.map_err(|_| Error::Timeout)
}
高度な非同期パターン
CancellationToken によるグレースフルシャットダウン
use tokio_util::sync::CancellationToken;
async fn run_server(token: CancellationToken) -> Result<()> {
let listener = TcpListener::bind("0.0.0.0:8080").await?;
loop {
tokio::select! {
Ok((stream, _)) = listener.accept() => {
let child_token = token.child_token();
tokio::spawn(async move {
handle_connection(stream, child_token).await;
});
}
_ = token.cancelled() => {
tracing::info!("shutdown signal received, draining connections");
break;
}
}
}
Ok(())
}
// Main: wire up OS signals to cancellation
#[tokio::main]
async fn main() -> Result<()> {
let token = CancellationToken::new();
let shutdown_token = token.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
shutdown_token.cancel();
});
run_server(token).await
}
JoinSet による構造化並行性
use tokio::task::JoinSet;
async fn process_batch(items: Vec<Item>) -> Vec<Result<Output>> {
let mut set = JoinSet::new();
for item in items {
set.spawn(async move { process_item(item).await });
}
let mut results = Vec::with_capacity(set.len());
while let Some(res) = set.join_next().await {
match res {
Ok(output) => results.push(output),
Err(join_err) => results.push(Err(join_err.into())),
}
}
results
}
有界チャネルによるバックプレッシャー
use tokio::sync::mpsc;
async fn pipeline(input: Vec<RawData>) -> Result<()> {
// Bound the channel to apply backpressure when consumer is slow
let (tx, mut rx) = mpsc::channel::<Processed>(64);
// Producer: blocks when channel is full
let producer = tokio::spawn(async move {
for item in input {
let processed = transform(item).await;
if tx.send(processed).await.is_err() {
break; // Receiver dropped, stop producing
}
}
});
// Consumer: processes at its own pace
while let Some(item) = rx.recv().await {
persist(item).await?;
}
producer.await?;
Ok(())
}
Tower ミドルウェアの合成
use tower::{ServiceBuilder, ServiceExt};
use tower_http::{trace::TraceLayer, timeout::TimeoutLayer};
// Stack middleware layers declaratively
let service = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.concurrency_limit(100)
.rate_limit(1000, Duration::from_secs(1))
.service(my_handler);
// Custom Tower Layer for retry with backoff
use tower::retry::{Retry, Policy};
#[derive(Clone)]
struct RetryPolicy {
max_retries: usize,
}
impl<Req: Clone, Res, E> Policy<Req, Res, E> for RetryPolicy {
type Future = futures::future::Ready<()>;
fn retry(&mut self, _req: &mut Req, result: &mut Result<Res, E>) -> Option<Self::Future> {
if self.max_retries > 0 && result.is_err() {
self.max_retries -= 1;
Some(futures::future::ready(()))
} else {
None
}
}
fn clone_request(&mut self, req: &Req) -> Option<Req> {
Some(req.clone())
}
}
規律あるワークフローの統合(非同期)
- 調査フェーズ:非同期処理の境界、キャンセル要件、バックプレッシャーの必要性を特定します
- 設計フェーズ:Tower ミドルウェアスタック、シャットダウン戦略、チャネルの容量を明確に定めます
- 検証フェーズ:キャンセル経路をユニットテストし、負荷がかかった状態でバックプレッシャーが機能することを確認し、ミドルウェアの順序をテストします
FFI と言語間統合
安全な C API ラッパー
// Opaque handle pattern: hide Rust internals behind a pointer
pub struct Engine { /* internal fields */ }
/// SAFETY: Engine is Send+Sync, and callers must not use
/// the handle after calling engine_destroy.
#[no_mangle]
pub extern "C" fn engine_create() -> *mut Engine {
Box::into_raw(Box::new(Engine::new()))
}
#[no_mangle]
pub extern "C" fn engine_process(
engine: *mut Engine,
input: *const c_char,
input_len: usize,
) -> i32 {
// Catch panics at every FFI boundary
std::panic::catch_unwind(|| {
let engine = unsafe {
assert!(!engine.is_null());
&mut *engine
};
let slice = unsafe { std::slice::from_raw_parts(input as *const u8, input_len) };
match std::str::from_utf8(slice) {
Ok(s) => engine.process(s).map(|_| 0).unwrap_or(-1),
Err(_) => -2, // Invalid UTF-8
}
})
.unwrap_or(-99) // Panic occurred
}
#[no_mangle]
pub extern "C" fn engine_destroy(engine: *mut Engine) {
if !engine.is_null() {
unsafe { drop(Box::from_raw(engine)); }
}
}
FFI をまたぐ文字列管理
use std::ffi::{CStr, CString};
// Receiving a C string (borrowed)
fn from_c_str(ptr: *const c_char) -> Result<&str> {
let cstr = unsafe { CStr::from_ptr(ptr) };
cstr.to_str().map_err(|_| Error::InvalidUtf8)
}
// Returning a string to C (caller must free)
#[no_mangle]
pub extern "C" fn engine_get_name(engine: *const Engine) -> *mut c_char {
let engine = unsafe { &*engine };
CString::new(engine.name())
.map(CString::into_raw)
.unwrap_or(std::ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn engine_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)); }
}
}
WASM 相互運用パターン
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmEngine {
inner: Engine,
}
#[wasm_bindgen]
impl WasmEngine {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self { inner: Engine::new() }
}
pub fn process(&mut self, input: &str) -> Result<JsValue, JsError> {
let result = self.inner.process(input)?;
Ok(serde_wasm_bindgen::to_value(&result)?)
}
}
// Conditional compilation for WASM vs native
#[cfg(target_arch = "wasm32")]
pub fn get_time() -> f64 {
js_sys::Date::now()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_time() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64() * 1000.0
}
バインディングの自動生成に uniffi を使用する
// In src/lib.rs with uniffi scaffolding
uniffi::setup_scaffolding!();
#[derive(uniffi::Object)]
pub struct SearchEngine { /* ... */ }
#[uniffi::export]
impl SearchEngine {
#[uniffi::constructor]
pub fn new(config: SearchConfig) -> Self { /* ... */ }
pub fn search(&self, query: &str) -> Vec<SearchResult> { /* ... */ }
}
#[derive(uniffi::Record)]
pub struct SearchConfig {
pub max_results: u32,
pub case_sensitive: bool,
}
規律あるワークフローの統合(FFI)
- 調査フェーズ:すべての
externブロックと unsafe な FFI を監査し、安全性の欠落を特定します - 設計フェーズ:すべての FFI 境界における所有権移転のセマンティクスを明確に定めます
- 検証フェーズ:新たな FFI サーフェスには Miri とファジングが必須です。文字列変換の往復にはプロパティテストを実施します
クレートの推奨事項
| カテゴリ | クレート | 用途 |
|---|---|---|
| 非同期ランタイム | tokio | 業界標準の非同期ランタイム |
| 非同期ユーティリティ | tokio-util | CancellationToken、コーデック用ヘルパー |
| ミドルウェア | tower | Service トレイト、レイヤー、リトライ、レート制限 |
| HTTP ミドルウェア | tower-http | HTTP 用のトレース、タイムアウト、圧縮レイヤー |
| シリアライズ | serde | シリアライズ/デシリアライズのフレームワーク |
| HTTP クライアント | reqwest | 非同期 HTTP クライアント |
| HTTP サーバー | axum | 使いやすい Web フレームワーク |
| CLI | clap | コマンドラインの解析 |
| ロギング | tracing | 構造化ロギング/トレーシング |
| エラー処理 | thiserror | Error 実装の導出 |
| エラーコンテキスト | anyhow | アプリケーションのエラー処理 |
| テスト | proptest | プロパティベーステスト |
| モック | mockall | モックの生成 |
| WASM バインディング | wasm-bindgen | WebAssembly 用の Rust から JS への FFI |
| WASM Serde | serde-wasm-bindgen | Rust の型を JsValue にシリアライズ |
| マルチ言語 FFI | uniffi | Kotlin/Swift/Python バインディングを自動生成 |
よくある落とし穴
clone()の多用 - 設計上の問題を示していることがよくあります- ライフタイムの無視 - 重要な制約を伝えるものです
- 非同期処理内でブロッキングする - CPU 処理には
spawn_blockingを使用します - ライブラリ内でパニックを起こす - 代わりにエラーを返します
- 文字列に依存した型付きでない API - newtype と enum を使用します
unsafe コードのポリシー
unsafe コードは必要な場合に限り許可され、厳格なガイドラインに従う必要があります:
要件
- unsafe を小さく、十分にテストされたモジュール境界の内側に隔離する
- すべての unsafe ブロックに以下を記載する:
- 成立していなければならない不変条件
- 安全な Rust ではこれを表現できない理由
- 正しさの検証方法(テスト、ファジング)
/// SAFETY: This module provides safe wrappers around raw pointer operations.
/// All public functions maintain the following invariants:
/// - Pointers are always valid and aligned
/// - Lifetimes are correctly bounded
/// - No data races possible (single-threaded access enforced)
mod raw_buffer {
/// SAFETY: `ptr` must be valid for reads of `len` bytes.
/// The caller must ensure the memory is initialized.
/// This is verified by: unit tests, property tests, and fuzzing.
pub unsafe fn read_unchecked(ptr: *const u8, len: usize) -> Vec<u8> {
// implementation
}
}
安全な代替手段を優先する
unsafe を記述する前に:
- 標準ライブラリに安全な API があるか確認します
- 十分に監査されたクレート(例:
zerocopy、bytemuck)を確認します - ベンチマークデータで必要性が示された場合に限り、独自のポインタ操作を使用します
unsafe コードのテスト
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_buffer_valid_input() {
// Test the happy path
}
#[test]
fn raw_buffer_empty_input() {
// Edge case: zero length
}
// Property-based test to find edge cases
proptest! {
#[test]
fn raw_buffer_never_panics(data: Vec<u8>) {
// Should never panic or cause UB
}
}
}
CLI/ツールのエラー処理
ユーザー向けツールでは、エラーを解決に役立つものにする必要があります:
// BAD: Unhelpful error
Err(Error::Failed)
// GOOD: Actionable error with context
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("config file not found at {path}: run `app init` to create one")]
NotFound { path: PathBuf },
#[error("invalid config at {path}:{line}: {message}")]
Parse { path: PathBuf, line: usize, message: String },
#[error("permission denied reading {path}: check file permissions with `ls -la {path}`")]
PermissionDenied { path: PathBuf },
}
ルール:
- エラーでは、何が失敗したのか、そしてどう修正するのかを説明する必要があります
- 公開 API には構造化エラー型(
thiserror)を使用します - I/O エラーや UTF-8 エラーを黙って無視してはいけません。動作を文書化し、テストしてください
- 呼び出しスタック全体でエラーコンテキストを保持します(アプリケーションには
anyhowを使用します)
制約
- ライブラリコードで
unwrap()を使用しない(理由を示したexpect()または適切なエラー処理を使用する) - 不変条件とテストを文書化せずに
unsafeを使用しない - 非同期コンテキストでブロッキング呼び出しを行わない
- ソースで
#![deny(warnings)]を使用しない(CI フラグを使用する) Rc/RefCellの使用を最小限にする(所有権を優先する)- 過度なジェネリクスを避ける(コンパイル時間に影響する)
- 懸念事項が異なる場合は、約 1000 行を目安にモジュールを分割する
成功指標
- 警告なしでコードがコンパイルされる
cargo fmt --checkとcargo clippyを問題なく通過する- すべての
#[allow(...)]注釈にコメントで正当な理由が示されている - パフォーマンスの低下がない(ベンチマークで検証する)
- unsafe コードが隔離され、文書化され、テストされている
- ユーザーにとって直感的な API である
- エンドユーザーにとってエラーメッセージが解決に役立つ
- ドキュメントが包括的である