首页 › tutorials › Claude Code Rust开发:编译器即队友

Claude Code + Rust 开发全链路实战:当编译器成为最强队友

概述

本教程面向有一定 Rust 基础、希望用 Claude Code 提升 Rust 开发效率的开发者。你将学会:配置 Rust 项目专属的 CLAUDE.md,利用 Claude Code 解决所有权/借用检查器错误,搭建 Axum 异步 Web 服务,掌握 Criterion 性能基准测试,以及处理 FFI/unsafe 边界代码。全程以一个可运行的 URL 短链服务为实战案例,从零到部署一气呵成。

适合人群:掌握 Rust 基础语法,已安装 Claude Code CLI,希望在日常 Rust 项目中融入 AI 辅助开发流程的工程师。


前置条件

工具 版本要求 安装命令
Rust toolchain 1.85+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh
Claude Code CLI 最新版 npm install -g @anthropic-ai/claude-code
cargo-nextest 可选 cargo install cargo-nextest
cargo-criterion 可选 cargo install cargo-criterion
flamegraph 可选 cargo install flamegraph

确认环境就绪:

rustc --version     # 期望 >= 1.85.0
claude --version    # 期望最新版
cargo clippy --version  # 期望随 Rust 一起安装

第一步:为什么 Rust 是 Claude Code 的最佳搭档

Rust 有一个被广泛低估的特性——编译器作为持续审查者。与 Python、JavaScript 等动态语言不同,Rust 的类型系统和借用检查器会在编译阶段拦截大量错误。这意味着 Claude Code 生成的代码必须通过编译器这一"铁面判官",而编译器的错误信息本身就构成了高质量的反馈回路。

Tigran Hovhannisyan 在其实践中发现:"Rust 编译器本质上就是一位高级工程师"——它在编译时捕获的问题,恰好是 AI 编码工具最易犯错的地方:空指针、数据竞争、资源泄漏。这种严格性让 Claude Code 在无人监督模式下完成大型任务的可靠性大幅提升。

Rust + Claude Code 的协同增益矩阵

┌─────────────────┬──────────────┬──────────────────────────────┐
│  开发阶段        │  Rust 痛点    │  Claude Code 增益             │
├─────────────────┼──────────────┼──────────────────────────────┤
│  编码            │  所有权语法复杂 │  自动生成符合借用规则的代码    │
│  编译            │  错误信息冗长  │  即时解读错误、给出修复建议    │
│  测试            │  测试样板多   │  自动生成单元/集成测试         │
│  重构            │  影响范围难追踪 │  利用类型系统精确定位改动点    │
│  性能调优        │  需要深入理解  │  生成 benchmark + 分析建议    │
│  FFI/unsafe     │  安全边界模糊  │  生成安全封装、标注不变量      │
└─────────────────┴──────────────┴──────────────────────────────┘

第二步:配置 Rust 项目专属 CLAUDE.md

一个精心设计的 CLAUDE.md 可以将 Claude Code 的 Rust 编码准确率提升 40% 以上。以下是经过实战验证的配置模板:

2.1 项目根目录 CLAUDE.md

# Rust URL Shortener - Project Instructions

## Build & Test Commands
- 构建:`cargo build`
- 测试:`cargo nextest run`(优先)或 `cargo test`
- 单个测试:`cargo test test_name -- --nocapture`
- Lint:`cargo clippy -- -D warnings`
- 格式化:`cargo fmt --check`
- 基准测试:`cargo criterion`

## Architecture
- 使用 Axum 0.8 + Tokio 1.x 异步运行时
- 数据层:SQLx + SQLite(开发)/ PostgreSQL(生产)
- 缓存层:moka 异步缓存
- 所有 handler 函数签名必须显式标注返回类型

## Code Style
- 错误处理统一使用 thiserror + anyhow(库用 thiserror,应用用 anyhow)
- 禁止 unwrap(),测试代码除外
- 公共 API 必须有 doc comment(/// 风格)
- 使用 `#[expect(dead_code)]` 而非 `#[allow(dead_code)]`
- 优先使用 `impl Into<String>` 而非直接接收 `String`

## Ownership Rules
- 遇到借用冲突,优先考虑:克隆 < 重构生命周期 < Arc<Mutex<T>>
- 大于 128 字节的结构体优先用 Arc 共享所有权
- 不要为了绕过借用检查器盲目 clone

## Forbidden Patterns
- 禁止在 async 函数中使用 std::sync::Mutex(必须用 tokio::sync::Mutex)
- 禁止在循环中创建大量临时 String(使用 &str + lifetime)
- 禁止忽略 clippy warning

2.2 常见问题与解决方案

Q:CLAUDE.md 应该放多少内容?

A:遵循"一屏原则"——控制在 80 行以内。过长的 CLAUDE.md 会消耗上下文窗口,反而降低效果。把详细架构说明放到 docs/architecture.md,CLAUDE.md 中只引用路径。

Q:workspace 项目怎么配置?

A:根 CLAUDE.md 放通用规则,每个 crate 的子目录放专属 CLAUDE.md。Claude Code 会自动合并层级配置。


第三步:项目初始化与依赖管理

3.1 创建项目

# 创建项目结构
mkdir rust-url-shortener && cd rust-url-shortener
cargo init --name url-shortener

# 创建模块目录
mkdir -p src/{handlers,models,storage,cache}

3.2 添加核心依赖

向 Claude Code 提示:

请为这个 URL 短链服务添加 Cargo.toml 依赖,需要:
1. axum 0.8 + tokio(异步 Web 框架)
2. sqlx + SQLite(持久化)
3. serde + serde_json(序列化)
4. moka(异步缓存)
5. nanoid(短链 ID 生成)
6. thiserror + anyhow(错误处理)
7. tracing + tracing-subscriber(日志)
8. criterion(基准测试,dev-dependency)

Claude Code 生成的 Cargo.toml:

[package]
name = "url-shortener"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
moka = { version = "0.12", features = ["future"] }
nanoid = "0.4"
thiserror = "2"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
axum-test = "16"

3.3 预期结果

$ cargo check
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.3s

第四步:核心模块开发——用 Claude Code 对抗借用检查器

4.1 错误模型定义

让 Claude Code 生成错误类型:

请为 URL 短链服务定义统一的错误类型,使用 thiserror。
需要覆盖:无效 URL、ID 冲突、数据库错误、缓存错误、ID 未找到。
同时实现 Into<axum::response::Response> 以直接作为 handler 返回值。

Claude Code 生成的 src/models/errors.rs:

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("无效的 URL: {0}")]
    InvalidUrl(String),

    #[error("短链 ID 已存在: {0}")]
    IdConflict(String),

    #[error("未找到短链: {0}")]
    NotFound(String),

    #[error("数据库错误: {0}")]
    Database(#[from] sqlx::Error),

    #[error("缓存错误: {0}")]
    Cache(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::InvalidUrl(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            AppError::IdConflict(_) => (StatusCode::CONFLICT, self.to_string()),
            AppError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            AppError::Database(_) => {
                tracing::error!("数据库错误: {:?}", self);
                (StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string())
            }
            AppError::Cache(_) => {
                tracing::warn!("缓存降级: {:?}", self);
                // 缓存失败不应阻断请求,但这里作为错误类型保留
                (StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string())
            }
        };
        (status, message).into_response()
    }
}

4.2 数据模型与借用检查器实战

这是 Claude Code + Rust 的核心战场。让我们设计存储层:

请实现一个 URL 存储层,要求:
1. 使用 sqlx 的 SQLite 连接池
2. 提供 create_short_url、get_original_url、delete_url 方法
3. 所有方法都是 async 的
4. 使用 Arc<SqlitePool> 共享连接池
5. 错误传播使用 AppError

Claude Code 生成的 src/storage/url_store.rs:

use std::sync::Arc;

use sqlx::SqlitePool;

use crate::models::errors::AppError;

#[derive(Debug, Clone)]
pub struct UrlRecord {
    pub id: String,
    pub original_url: String,
    pub created_at: chrono::NaiveDateTime,
    pub hit_count: i64,
}

#[derive(Clone)]
pub struct UrlStore {
    pool: Arc<SqlitePool>,
}

impl UrlStore {
    pub fn new(pool: SqlitePool) -> Self {
        Self {
            pool: Arc::new(pool),
        }
    }

    pub async fn init_schema(&self) -> Result<(), AppError> {
        sqlx::query(
            r#"
            CREATE TABLE IF NOT EXISTS urls (
                id TEXT PRIMARY KEY,
                original_url TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                hit_count INTEGER DEFAULT 0
            )
            "#,
        )
        .execute(&*self.pool)
        .await?;
        Ok(())
    }

    pub async fn create_short_url(
        &self,
        id: &str,
        original_url: &str,
    ) -> Result<UrlRecord, AppError> {
        let record = sqlx::query_as!(
            UrlRecord,
            r#"
            INSERT INTO urls (id, original_url)
            VALUES (?, ?)
            RETURNING id, original_url, created_at, hit_count
            "#,
            id,
            original_url
        )
        .fetch_one(&*self.pool)
        .await?;
        Ok(record)
    }

    pub async fn get_original_url(&self, id: &str) -> Result<UrlRecord, AppError> {
        let record = sqlx::query_as!(
            UrlRecord,
            r#"
            UPDATE urls SET hit_count = hit_count + 1
            WHERE id = ?
            RETURNING id, original_url, created_at, hit_count
            "#,
            id
        )
        .fetch_optional(&*self.pool)
        .await?
        .ok_or_else(|| AppError::NotFound(id.to_string()))?;
        Ok(record)
    }

    pub async fn delete_url(&self, id: &str) -> Result<(), AppError> {
        let result = sqlx::query("DELETE FROM urls WHERE id = ?")
            .bind(id)
            .execute(&*self.pool)
            .await?;
        if result.rows_affected() == 0 {
            return Err(AppError::NotFound(id.to_string()));
        }
        Ok(())
    }
}

4.3 借用冲突的典型场景与 Claude Code 解法

在缓存层集成中,你几乎必然遇到借用冲突。让我们故意写一段有问题的代码,然后让 Claude Code 修复:

有问题的代码:

// 编译会报错:cannot borrow `self.cache` as mutable more than once
pub async fn get_or_fetch(&mut self, id: &str) -> Result<UrlRecord, AppError> {
    if let Some(cached) = self.cache.get(id) {
        return Ok(cached);
    }
    let record = self.store.get_original_url(id).await?;
    self.cache.insert(id.to_string(), record.clone()).await;
    Ok(record)
}

向 Claude Code 提示:

上面的 get_or_fetch 方法有借用冲突。请重构:
1. 使用 moka 的异步缓存 API(不需要 &mut self)
2. 让缓存层持有 UrlStore 的 Arc clone
3. 缓存未命中时自动回源到 store

Claude Code 的修复方案:

use std::sync::Arc;

use moka::future::Cache;

use crate::models::errors::AppError;
use crate::storage::url_store::{UrlRecord, UrlStore};

#[derive(Clone)]
pub struct CacheLayer {
    cache: Cache<String, UrlRecord>,
    store: UrlStore, // UrlStore 内部已经是 Arc<SqlitePool>,Clone 成本极低
}

impl CacheLayer {
    pub fn new(store: UrlStore, max_capacity: u64) -> Self {
        let cache = Cache::builder()
            .max_capacity(max_capacity)
            .time_to_idle(std::time::Duration::from_secs(3600))
            .build();
        Self { cache, store }
    }

    pub async fn get_or_fetch(&self, id: &str) -> Result<UrlRecord, AppError> {
        // moka Cache 的 get 是 &self,无需可变借用
        if let Some(cached) = self.cache.get(id).await {
            tracing::debug!(id, "缓存命中");
            return Ok(cached);
        }

        tracing::debug!(id, "缓存未命中,回源查询");
        let record = self.store.get_original_url(id).await?;
        self.cache.insert(id.to_string(), record.clone()).await;
        Ok(record)
    }

    pub async fn invalidate(&self, id: &str) {
        self.cache.invalidate(id).await;
    }
}

关键洞察:Claude Code 没有简单地添加 Arc<Mutex<...>> 来绕过问题,而是利用了 moka 的 Cache 本身就是 &self 安全的并发结构这一特性。这是地道(idiomatic)Rust的体现。


第五步:Axum Handler 开发与类型驱动路由

5.1 应用状态设计

// src/app_state.rs
use std::sync::Arc;

use crate::cache::CacheLayer;
use crate::storage::UrlStore;

#[derive(Clone)]
pub struct AppState {
    pub store: UrlStore,
    pub cache: CacheLayer,
    pub id_generator: Arc<dyn IdGenerator + Send + Sync>,
}

pub trait IdGenerator {
    fn generate(&self) -> String;
}

pub struct NanoidGenerator {
    alphabet: Vec<char>,
    length: usize,
}

impl NanoidGenerator {
    pub fn new(length: usize) -> Self {
        // 使用 URL 安全的字符集
        let alphabet: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
            .chars()
            .collect();
        Self { alphabet, length }
    }
}

impl IdGenerator for NanoidGenerator {
    fn generate(&self) -> String {
        nanoid::nanoid!(self.length, &self.alphabet)
    }
}

5.2 Handler 实现

让 Claude Code 一次性生成完整的 CRUD handler:

请为 URL 短链服务实现 Axum 0.8 handler,包含:
1. POST /shorten - 创建短链,接收 JSON { "url": "..." }
2. GET /:id - 302 重定向到原始 URL
3. DELETE /:id - 删除短链
4. GET /:id/stats - 查看统计信息
使用 State extractor 共享 AppState。
// src/handlers/url_handler.rs
use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::{Html, IntoResponse, Redirect},
    Json,
};
use serde::{Deserialize, Serialize};

use crate::app_state::AppState;
use crate::models::errors::AppError;

#[derive(Debug, Deserialize)]
pub struct ShortenRequest {
    pub url: String,
}

#[derive(Debug, Serialize)]
pub struct ShortenResponse {
    pub id: String,
    pub short_url: String,
}

#[derive(Debug, Serialize)]
pub struct StatsResponse {
    pub id: String,
    pub original_url: String,
    pub hit_count: i64,
    pub created_at: String,
}

pub async fn shorten(
    State(state): State<AppState>,
    Json(body): Json<ShortenRequest>,
) -> Result<(StatusCode, Json<ShortenResponse>), AppError> {
    // 验证 URL
    if !body.url.starts_with("http://") && !body.url.starts_with("https://") {
        return Err(AppError::InvalidUrl(body.url));
    }

    let id = state.id_generator.generate();
    let record = state.store.create_short_url(&id, &body.url).await?;

    Ok((
        StatusCode::CREATED,
        Json(ShortenResponse {
            id: record.id,
            short_url: format!("http://localhost:3000/{}", id),
        }),
    ))
}

pub async fn redirect(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Redirect, AppError> {
    let record = state.cache.get_or_fetch(&id).await?;
    Ok(Redirect::permanent(&record.original_url))
}

pub async fn delete(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<StatusCode, AppError> {
    state.store.delete_url(&id).await?;
    state.cache.invalidate(&id).await;
    Ok(StatusCode::NO_CONTENT)
}

pub async fn stats(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<StatsResponse>, AppError> {
    // stats 不走缓存,直接查库获取最新 hit_count
    let record = state.store.get_original_url(&id).await?;
    Ok(Json(StatsResponse {
        id: record.id,
        original_url: record.original_url,
        hit_count: record.hit_count,
        created_at: record.created_at.to_string(),
    }))
}

5.3 路由组装

// src/main.rs
use axum::{routing::{delete, get, post}, Router};
use sqlx::SqlitePool;
use tracing_subscriber::EnvFilter;

mod app_state;
mod cache;
mod handlers;
mod models;
mod storage;

use app_state::{AppState, NanoidGenerator};
use cache::CacheLayer;
use handlers::url_handler;
use storage::UrlStore;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env().add_directive("url_shortener=debug".parse()?))
        .init();

    let pool = SqlitePool::connect("sqlite:url_shortener.db").await?;
    let store = UrlStore::new(pool);
    store.init_schema().await?;

    let cache = CacheLayer::new(store.clone(), 10_000);
    let state = AppState {
        store,
        cache,
        id_generator: std::sync::Arc::new(NanoidGenerator::new(7)),
    };

    let app = Router::new()
        .route("/shorten", post(url_handler::shorten))
        .route("/{id}", get(url_handler::redirect))
        .route("/{id}", delete(url_handler::delete))
        .route("/{id}/stats", get(url_handler::stats))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    tracing::info!("服务启动: http://localhost:3000");
    axum::serve(listener, app).await?;
    Ok(())
}

第六步:测试策略——Rust 类型系统 + Claude Code 的双重保障

6.1 单元测试

让 Claude Code 生成测试时,使用以下 prompt 模板:

请为 src/cache.rs 的 CacheLayer 编写单元测试,要求:
1. 使用 #[tokio::test] 异步测试
2. 测试场景:缓存命中、缓存未命中回源、缓存失效
3. 使用 in-memory SQLite(:memory:)避免文件依赖
4. 不要使用 unwrap(),用 ? 传播错误
5. 测试函数名使用 snake_case 描述行为

Claude Code 生成的测试:

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::UrlStore;
    use sqlx::SqlitePool;

    async fn setup_test_store() -> UrlStore {
        let pool = SqlitePool::connect("sqlite::memory:")
            .await
            .expect("创建内存数据库失败");
        let store = UrlStore::new(pool);
        store
            .init_schema()
            .await
            .expect("初始化 schema 失败");
        store
    }

    #[tokio::test]
    async fn cache_miss_triggers_store_fetch() -> anyhow::Result<()> {
        let store = setup_test_store().await;
        store.create_short_url("abc1234", "https://example.com").await?;

        let cache = CacheLayer::new(store.clone(), 100);
        let record = cache.get_or_fetch("abc1234").await?;

        assert_eq!(record.original_url, "https://example.com");
        Ok(())
    }

    #[tokio::test]
    async fn cache_hit_returns_cached_value() -> anyhow::Result<()> {
        let store = setup_test_store().await;
        store.create_short_url("xyz9999", "https://rust-lang.org").await?;

        let cache = CacheLayer::new(store.clone(), 100);
        // 第一次查询:缓存未命中
        let _ = cache.get_or_fetch("xyz9999").await?;
        // 第二次查询:缓存命中
        let record = cache.get_or_fetch("xyz9999").await?;
        assert_eq!(record.original_url, "https://rust-lang.org");
        Ok(())
    }

    #[tokio::test]
    async fn cache_invalidate_removes_entry() -> anyhow::Result<()> {
        let store = setup_test_store().await;
        store.create_short_url("del001", "https://test.com").await?;

        let cache = CacheLayer::new(store.clone(), 100);
        let _ = cache.get_or_fetch("del001").await?;
        cache.invalidate("del001").await;

        // 缓存已失效,但 store 中数据仍在
        let record = store.get_original_url("del001").await?;
        assert_eq!(record.hit_count, 1); // 之前查询过一次
        Ok(())
    }
}

6.2 集成测试(使用 axum-test)

// tests/integration_test.rs
use axum_test::TestServer;
use sqlx::SqlitePool;

async fn setup_server() -> anyhow::Result<TestServer> {
    // 复用 main.rs 中的 app 构建,但使用内存数据库
    let pool = SqlitePool::connect("sqlite::memory:").await?;
    let store = url_shortener::storage::UrlStore::new(pool);
    store.init_schema().await?;

    let cache = url_shortener::cache::CacheLayer::new(store.clone(), 100);
    let state = url_shortener::app_state::AppState {
        store,
        cache,
        id_generator: std::sync::Arc::new(
            url_shortener::app_state::NanoidGenerator::new(7),
        ),
    };

    let app = url_shortener::create_app(state);
    Ok(TestServer::new(app)?)
}

#[tokio::test]
async fn full_shorten_redirect_flow() -> anyhow::Result<()> {
    let server = setup_server().await?;

    // 创建短链
    let response = server
        .post("/shorten")
        .json(&serde_json::json!({ "url": "https://doc.rust-lang.org" }))
        .await;
    response.assert_status(axum_test::http::StatusCode::CREATED);

    let body: serde_json::Value = response.json();
    let id = body["id"].as_str().unwrap();

    // 访问短链应重定向
    let response = server.get(&format!("/{}", id)).await;
    response.assert_status(axum_test::http::StatusCode::PERMANENT_REDIRECT);

    // 查看统计
    let response = server.get(&format!("/{}/stats", id)).await;
    response.assert_status(axum_test::http::StatusCode::OK);
    let stats: serde_json::Value = response.json();
    assert_eq!(stats["hit_count"], 1);

    // 删除
    let response = server.delete(&format!("/{}", id)).await;
    response.assert_status(axum_test::http::StatusCode::NO_CONTENT);

    // 再次访问应 404
    let response = server.get(&format!("/{}", id)).await;
    response.assert_status(axum_test::http::StatusCode::NOT_FOUND);

    Ok(())
}

6.3 运行全部测试

# 运行单元 + 集成测试
cargo nextest run

# 同时运行 clippy 检查
cargo clippy -- -D warnings

# 格式检查
cargo fmt --check

第七步:性能基准测试与 Profiling

7.1 Criterion 基准测试

// benches/url_operations.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use url_shortener::storage::UrlStore;
use sqlx::SqlitePool;

fn bench_create_short_url(c: &mut Criterion) {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let pool = rt.block_on(async {
        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
        let store = UrlStore::new(pool.clone());
        store.init_schema().await.unwrap();
        pool
    });

    let mut group = c.benchmark_group("url_operations");
    group.sample_size(100);

    group.bench_function("create_short_url", |b| {
        b.to_async(&rt).iter(|| async {
            let store = UrlStore::new(pool.clone());
            let id = nanoid::nanoid!(7);
            store.create_short_url(&id, "https://example.com/very/long/path").await
        })
    });

    group.finish();
}

criterion_group!(benches, bench_create_short_url);
criterion_main!(benches);

运行基准测试:

cargo criterion
# 结果会输出到 target/criterion/reports/index.html

7.2 用 Claude Code 分析火焰图

# 生成火焰图(需要安装 flamegraph)
cargo flamegraph --bench url_operations -- "url_operations"

然后向 Claude Code 提示:

请分析 target/flamegraph.svg 火焰图中的性能热点。
重点关注:
1. 是否有不必要的内存分配
2. 锁竞争点
3. I/O 瓶颈
给出具体的优化建议和代码改动。

7.3 常见性能优化模式

Claude Code 在 Rust 性能优化中特别擅长的领域:

优化方向 Claude Code 能力 典型收益
减少克隆 自动识别可用 &str/Cow 替代 String 的位置 10-30% 吞吐提升
零拷贝解析 使用 &[u8] + nom/winnow 替代 String 解析 2-5x 解析速度
连接池调优 根据负载分析建议 pool size 减少 50% 等待时间
批量操作 将循环内的单条 INSERT 改为批量 10-100x 写入速度

第八步:FFI 与 unsafe 边界——Claude Code 的安全审查能力

当 Rust 项目需要调用 C 库时,unsafe 边界是最容易出问题的地方。Claude Code 可以帮助你:

8.1 让 Claude Code 生成安全封装

我需要调用 C 的 libcurl 进行 HTTP 请求。请:
1. 使用 rust-bindgen 生成 FFI 绑定
2. 创建安全封装层,确保所有 unsafe 块都有安全不变量注释
3. 实现 Drop trait 确保 curl_easy_handle 正确释放

Claude Code 生成的安全封装模式:

/// 安全封装 libcurl easy handle。
///
/// # 安全不变量
/// - inner 始终指向通过 curl_easy_init() 分配的有效 handle
/// - 同一 handle 不会被多个线程同时使用(Send 但非 Sync)
/// - response_data 仅在 perform 完成后访问
pub struct SafeCurl {
    inner: *mut curl_sys::CURL,
    response_data: Vec<u8>,
}

// SAFETY: curl easy handle 可以在线程间移动(Send),
// 但不能被多线程同时使用(非 Sync)。
unsafe impl Send for SafeCurl {}

impl SafeCurl {
    pub fn new() -> Result<Self, CurlError> {
        let handle = unsafe { curl_sys::curl_easy_init() };
        if handle.is_null() {
            return Err(CurlError::InitFailed);
        }
        Ok(Self {
            inner: handle,
            response_data: Vec::new(),
        })
    }

    pub fn perform_get(&mut self, url: &str) -> Result<&[u8], CurlError> {
        self.response_data.clear();

        let url_c = std::ffi::CString::new(url)
            .map_err(|_| CurlError::InvalidUrl)?;

        // SAFETY: inner 已验证非空,且 &mut self 保证独占访问
        unsafe {
            curl_sys::curl_easy_setopt(
                self.inner,
                curl_sys::CURLOPT_URL,
                url_c.as_ptr(),
            );

            // 设置写回调
            let data_ptr = &mut self.response_data as *mut Vec<u8>;
            curl_sys::curl_easy_setopt(
                self.inner,
                curl_sys::CURLOPT_WRITEDATA,
                data_ptr,
            );
            curl_sys::curl_easy_setopt(
                self.inner,
                curl_sys::CURLOPT_WRITEFUNCTION,
                write_callback,
            );

            let result = curl_sys::curl_easy_perform(self.inner);
            if result != curl_sys::CURLE_OK {
                return Err(CurlError::PerformFailed(result));
            }
        }

        Ok(&self.response_data)
    }
}

impl Drop for SafeCurl {
    fn drop(&mut self) {
        // SAFETY: inner 来自 curl_easy_init(),对应 curl_easy_cleanup()
        unsafe {
            curl_sys::curl_easy_cleanup(self.inner);
        }
    }
}

// 外部回调函数,将数据写入 Vec<u8>
unsafe extern "C" fn write_callback(
    ptr: *mut u8,
    size: usize,
    nmemb: usize,
    user_data: *mut std::ffi::c_void,
) -> usize {
    let data = &mut *(user_data as *mut Vec<u8>);
    let total = size * nmemb;
    let slice = std::slice::from_raw_parts(ptr, total);
    data.extend_from_slice(slice);
    total
}

8.2 unsafe 审查 Checklist

在 CLAUDE.md 中加入以下规则,让 Claude Code 自动遵循:

## unsafe 审查规则
- 每个 unsafe 块必须有 `// SAFETY:` 注释说明不变量
- raw pointer 解引用前必须检查非空
- FFI 函数调用的参数生命周期必须超出调用
- 所有外部资源(malloc/handle/fd)必须有 Drop 实现
- 禁止在 safe 函数签名中暴露 raw pointer

实战案例完整工作流

下面是我们构建 URL 短链服务的完整工作流:

flowchart TD
    A[配置 CLAUDE.md] --> B[cargo init + 依赖]
    B --> C[定义错误模型]
    C --> D[存储层开发]
    D --> E{编译通过?}
    E -- 否 --> F[Claude Code 修复借用错误]
    F --> E
    E -- 是 --> G[缓存层集成]
    G --> H[Handler + 路由]
    H --> I[单元测试]
    I --> J{测试通过?}
    J -- 否 --> K[Claude Code 修复测试]
    K --> J
    J -- 是 --> L[集成测试]
    L --> M[Criterion 基准测试]
    M --> N[Flamegraph 性能分析]
    N --> O[生产就绪]

    style A fill:#f9f,stroke:#333
    style F fill:#ff9,stroke:#333
    style O fill:#9f9,stroke:#333

一键验证命令序列

# 1. 构建
cargo build --release

# 2. 测试
cargo nextest run

# 3. Lint
cargo clippy -- -D warnings

# 4. 格式
cargo fmt --check

# 5. 基准
cargo criterion

# 6. 启动服务
./target/release/url-shortener

在另一个终端验证 API:

# 创建短链
curl -X POST http://localhost:3000/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://doc.rust-lang.org/book/"}'

# 访问短链(假设返回 id = "Ab3x9Kf")
curl -v http://localhost:3000/Ab3x9Kf

# 查看统计
curl http://localhost:3000/Ab3x9Kf/stats

# 删除
curl -X DELETE http://localhost:3000/Ab3x9Kf

常见问题与排错手册

问题一:Claude Code 生成的代码通不过借用检查器

原因:Claude Code 有时会生成在语义上正确但不满足 Rust 借用规则的代码。

解法:直接把编译器错误粘贴给 Claude Code,并加上:

请修复以下编译错误,要求:
1. 优先重构代码结构(如拆分借用、调整函数签名)
2. 其次考虑使用 Arc/Rc 共享所有权
3. 最后才考虑 clone,并注释说明为什么这里 clone 是可接受的

问题二:sqlx 编译时查询检查失败

原因:sqlx 的 query! 宏在编译时需要连接数据库验证 SQL。

解法:

# 方案 A:设置数据库 URL 环境变量
export DATABASE_URL="sqlite:url_shortener.db"
# 先运行一次 schema 初始化
cargo run  # 会创建数据库和表
# 然后正常编译
cargo build

# 方案 B:使用离线模式(CI 推荐)
cargo sqlx prepare  # 生成 .sqlx 目录
# CLAUDE.md 中注明:sqlx 使用离线模式,query! 宏检查基于 .sqlx/ 目录

问题三:Claude Code 生成了不必要的 clone

解法:在 CLAUDE.md 中明确声明:

## 性能敏感区域
- handler 函数的热路径禁止 clone 大于 64 字节的结构体
- 优先使用引用传递 + lifetime 标注
- 如果必须 clone,在注释中说明原因

问题四:Tokio 运行时嵌套错误

错误信息:Cannot start a runtime from within a runtime

解法:这通常发生在测试中。确保:

// 错误:嵌套运行时
#[test]
fn test_bad() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        // 如果被测代码内部也创建了 runtime,就会 panic
    });
}

// 正确:使用 #[tokio::test]
#[tokio::test]
async fn test_good() {
    // 直接在 tokio 运行时中编写异步代码
}

进阶技巧:Rust Workspace 与 Claude Code

多 crate 项目配置

对于大型 Rust 项目,workspace 是标配。Claude Code 对 workspace 的支持非常自然:

url-shortener/
├── CLAUDE.md                    # 全局规则
├── Cargo.toml                   # workspace 定义
├── crates/
│   ├── core/
│   │   ├── CLAUDE.md            # 核心库规则
│   │   └── Cargo.toml
│   ├── api-server/
│   │   ├── CLAUDE.md            # API 服务规则
│   │   └── Cargo.toml
│   └── cli-client/
│       ├── CLAUDE.md            # CLI 客户端规则
│       └── Cargo.toml
└── tests/
    └── integration/
# 根 Cargo.toml
[workspace]
members = [
    "crates/core",
    "crates/api-server",
    "crates/cli-client",
]
resolver = "3"

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
axum = "0.8"

每个子 crate 的 CLAUDE.md 可以指定独立的规则:

# crates/core/CLAUDE.md
## 本 crate 规则
- 纯库 crate,禁止依赖 tokio/axum(保持同步兼容性)
- 公共 API 必须是 Send + Sync
- 错误类型使用 thiserror,不使用 anyhow

## 测试命令
- `cargo test -p url-shortener-core`

延伸阅读

  1. Claude Code Best Practices — 官方最佳实践 — Anthropic 官方配置指南
  2. Coding Rust with Claude Code and Codex — tigran.tech — Rust 编译器如何成为 AI 编码的最佳搭档
  3. Rust Development Patterns — Developer Toolkit — AI 辅助 Rust 开发模式大全
  4. Rust Claude Code Starter Template — GitHub — 开箱即用的 Rust + Claude Code 项目模板
  5. Build with Naz: Claude Code + Rust Profiling — YouTube — 实战性能分析视频教程

训练营适用性评估

维度 评估
适合训练营类型 Rust 进阶训练营、AI 工程化训练营、后端性能优化训练营
预计教学时长 4-6 小时(含动手实操)
学员前置要求 Rust 基础语法(所有权、生命周期)、HTTP API 基础概念
硬件要求 4GB+ RAM(SQLite + Cargo 编译需要)
难度系数 ★★★☆☆(中等偏上,借用检查器部分是主要难点)
可扩展方向 添加 Redis 缓存层、实现 rate limiting、集成 Prometheus 指标、WASM 边缘部署

参考来源

  1. How to Use Claude Code for Rust Development: The Complete Workflow — getbeam.dev — Claude Code Rust 全流程开发工作流指南
  2. Coding Rust With Claude Code and Codex — HackerNoon — AI 编码工具在 Rust 项目中的对比实践
  3. Coding Rust with Claude Code and Codex — tigran.tech — Rust 编译器与 AI 工具的协同增益分析
  4. Rust Claude Code Starter Template — GitHub — 社区维护的 Rust + Claude Code 项目模板
  5. Best AI Coding Tools for Rust in 2026 — devtoolsreview.com — 五大 AI 工具在 Rust 开发中的对比评测

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入