Rust 项目工程化脚手架指南

以 Embers 项目为例,介绍现代 Rust (2024 Edition) 的项目结构、工具链配置、代码规范等工程化最佳实践

Rust 项目工程化脚手架指南

以 Embers 项目为例,涵盖现代 Rust (2024 Edition) 的最佳实践


目录

  1. Workspace 统一依赖管理
  2. Cargo 命令使用时机
  3. 项目结构:DDD 分层架构
  4. TypeDD:类型驱动开发
  5. 错误处理策略
  6. Examples 编写规范
  7. Tests 编写规范
  8. 文档注释规范
  9. CI/CD 常用命令速查

1. Workspace 统一依赖管理

1.1 根 Cargo.toml 结构

# Cargo.toml (workspace root)

[workspace]
resolver = "2"  # Rust 2021+ 必须使用 v2 resolver

members = [
    "crates/embers-client",
    "crates/embers-server",
    "crates/embers-stream",
    "crates/embers-proto",
]

# ========== 统一元数据 ==========
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["Your Name <email@example.com>"]
license = "MIT"
repository = "https://github.com/user/repo"

# ========== 统一依赖版本 ==========
[workspace.dependencies]
# 异步运行时
tokio = { version = "1.49", features = ["full", "tracing"] }

# 序列化
serde = { version = "1", features = ["derive"] }
bincode = "1.0"

# 错误处理
thiserror = "2.0"  # 库:精确错误
anyhow = "1.0"     # 应用:错误传播

# 日志
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# ========== 内部 crate 引用 ==========
embers-proto = { path = "crates/embers-proto" }
embers-stream = { path = "crates/embers-stream" }

1.2 子 Crate 引用方式

# crates/embers-server/Cargo.toml

[package]
name = "embers-server"
version.workspace = true      # 继承 workspace 版本
edition.workspace = true      # 继承 workspace edition

[dependencies]
# 从 workspace 继承,保证版本一致
tokio = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

# 引用内部 crate
embers-proto = { workspace = true }
embers-stream = { workspace = true }

# crate 独有依赖(不需要统一)
some-specific-lib = "1.0"

1.3 优势

优势说明
版本一致性所有 crate 使用相同版本的 tokio/serde,避免冲突
减少编译时间相同版本的依赖只编译一次
简化升级升级时只需修改根 Cargo.toml
强制规范新 crate 必须遵循 workspace 约定

2. Cargo 命令使用时机

2.1 开发阶段命令

命令时机说明
cargo check频繁最快,只检查类型错误,不生成代码
cargo check -p <crate>频繁只检查特定 crate,更快
cargo clippy提交前检查代码风格和潜在问题
cargo test写完功能运行单元测试
cargo test -p <crate>调试时只测试特定 crate
cargo build需要运行时生成 debug 二进制
cargo run -p <bin>调试时运行特定二进制

2.2 提交前检查流程

# 1. 快速类型检查
cargo check --workspace --all-targets

# 2. Lint 检查
cargo clippy --workspace --all-targets -- -D warnings

# 3. 运行测试
cargo test --workspace

# 4. 格式化检查
cargo fmt -- --check

2.3 性能对比

cargo check        ████████░░  (8s)  - 推荐:开发时
cargo build        ████████████████ (16s) - 需要:运行时
cargo build --release ████████████████████████ (24s) - 发布

2.4 常用组合命令

# 只检查特定 crate(节省时间)
cargo check -p embers-server

# 运行特定 example
cargo run -p embers-stream --example capture_encode

# 运行特定测试
cargo test -p embers-stream --test test_quic

# 查看依赖树
cargo tree -p embers-client

# 检查过时依赖
cargo outdated  # 需要 cargo-outdated

3. 项目结构:DDD 分层架构

3.1 目录结构

embers/
├── Cargo.toml                 # Workspace 根配置
├── CLAUDE.md                  # AI 协作指南
│
└── crates/
    ├── embers-proto/          # 【协议层】共享内核
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       └── *.rs           # 纯数据结构
    │
    ├── embers-stream/         # 【流媒体核心】
    │   ├── Cargo.toml
    │   ├── examples/          # 可执行示例
    │   │   ├── capture_encode.rs
    │   │   └── test_client.rs
    │   └── src/
    │       ├── lib.rs
    │       ├── domain.rs      # 领域层导出
    │       ├── infra.rs       # 基础设施层导出
    │       ├── domain/        # 【领域层】纯逻辑
    │       │   ├── capture.rs   # CaptureSource trait
    │       │   ├── network.rs   # NetworkTransmitter trait
    │       │   ├── frame.rs     # Frame, FrameNumber 等
    │       │   └── error.rs     # 领域错误
    │       └── infra/         # 【基础设施层】IO 实现
    │           ├── cgdisplay.rs # macOS 屏幕捕获
    │           ├── encoder.rs   # GStreamer 编码
    │           └── quic.rs      # QUIC 网络实现
    │
    ├── embers-server/         # 【服务端】
    │   └── src/
    │       ├── domain/        # SessionManager, StreamTaskManager
    │       └── infra/         # QuicServer, 具体实现
    │
    └── embers-client/         # 【客户端】
        └── src/
            ├── domain/        # JitterBuffer, Receiver 状态机
            ├── infra/         # H264Decoder, StreamReceiver
            └── client.rs      # FFI 整合层

3.2 分层规则(单向依赖)

┌─────────────────────────────────────────────────┐
│                 基础设施层 (Infra)               │
│  GStreamer, QUIC, CGDisplay, wgpu, evdev       │
│  实现 Domain 定义的 Trait                       │
└──────────────────────┬──────────────────────────┘
                       │ 依赖
                       ▼
┌─────────────────────────────────────────────────┐
│                   领域层 (Domain)                │
│  CaptureSource, NetworkTransmitter, Frame       │
│  纯逻辑,无 IO,可单元测试                       │
└──────────────────────┬──────────────────────────┘
                       │ 依赖
                       ▼
┌─────────────────────────────────────────────────┐
│                   协议层 (Proto)                 │
│  纯数据结构 + Serde,无逻辑                      │
│  客户端/服务端共享                               │
└─────────────────────────────────────────────────┘

3.3 lib.rs 组织方式(Rust 2024 风格)

// src/lib.rs
// ❌ 不要使用 mod.rs(Rust 2015 风格)
// ✅ 使用目录同名文件

#![allow(async_fn_in_trait)]  // 模块级配置

pub mod domain;   // 对应 src/domain.rs + src/domain/
pub mod infra;    // 对应 src/infra.rs + src/infra/
// src/domain.rs(目录同名文件,导出子模块)
pub mod capture;
pub mod network;
pub mod frame;
pub mod error;

// 重导出常用类型,简化调用
pub use capture::{CaptureConfig, CaptureSource};
pub use frame::{Frame, FrameNumber, FrameRate, Resolution};

4. TypeDD:类型驱动开发

4.1 NewType 模式(防止参数混淆)

// ❌ 错误:容易搞反参数顺序
fn resize(width: u32, height: u32);
resize(1920, 1080);  // 还是 resize(1080, 1920)?

// ✅ 正确:使用 NewType
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Width(u32);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Height(u32);

fn resize(width: Width, height: Height);
resize(Width(1920), Height(1080));  // 编译器帮我们检查

4.2 实际示例:Resolution

// crates/embers-stream/src/domain/frame.rs

/// 分辨率,使用 NewType 模式保证有效性。
///
/// # Type Safety
///
/// 无法构造无效分辨率(如 0x0)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Resolution {
    width: u32,
    height: u32,
}

impl Resolution {
    /// 创建新分辨率。
    ///
    /// # Errors
    ///
    /// 宽高必须 > 0,否则返回 `None`。
    pub fn new(width: u32, height: u32) -> Option<Self> {
        if width > 0 && height > 0 {
            Some(Self { width, height })
        } else {
            None
        }
    }

    #[inline]
    pub const fn width(&self) -> u32 {
        self.width
    }

    #[inline]
    pub const fn height(&self) -> u32 {
        self.height
    }
}

4.3 Parse, Don’t Validate(解析而非校验)

// ❌ 错误:到处校验
fn process_frame(frame: &[u8]) {
    if frame.len() < 4 {
        return;
    }
    // ... 使用 frame
}

fn send_frame(frame: &[u8]) {
    if frame.len() < 4 {
        return;
    }
    // ... 发送 frame
}

// ✅ 正确:类型保证有效性
pub struct ValidatedFrame(Vec<u8>);

impl ValidatedFrame {
    /// 解析并验证帧数据。
    ///
    /// # Errors
    ///
    /// 帧数据无效时返回错误。
    pub fn parse(data: Vec<u8>) -> Result<Self, FrameError> {
        if data.len() < 4 {
            return Err(FrameError::TooShort);
        }
        Ok(Self(data))
    }
}

fn process(frame: &ValidatedFrame) {
    // 无需校验,类型已保证有效性
}

4.4 状态机模式

/// 接收器状态机。
///
/// 通过类型系统防止非法状态转换。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReceiverState {
    #[default]
    Disconnected,
    Connecting,
    Connected,
    Receiving,
    Error,
}

impl ReceiverState {
    /// 尝试转换到目标状态。
    ///
    /// # Errors
    ///
    /// 非法转换时返回错误。
    pub fn transition_to(self, target: Self) -> Result<Self, StateError> {
        use ReceiverState::*;
        match (self, target) {
            (Disconnected, Connecting) => Ok(Connecting),
            (Connecting, Connected) => Ok(Connected),
            (Connected, Receiving) => Ok(Receiving),
            (_, Error) => Ok(Error),  // 任何状态都可以转到 Error
            _ => Err(StateError::InvalidTransition { from: self, to: target }),
        }
    }
}

5. 错误处理策略

5.1 库使用 thiserror

// crates/embers-stream/src/domain/error.rs

/// 捕获相关错误。
#[derive(Debug, thiserror::Error)]
pub enum CaptureError {
    #[error("Invalid resolution: {width}x{height}")]
    InvalidResolution { width: u32, height: u32 },

    #[error("Capture not started")]
    NotStarted,

    #[error("Platform error: {0}")]
    Platform(#[from] std::io::Error),
}

5.2 应用/二进制使用 anyhow

// examples/capture_encode.rs

use anyhow::Result;

fn main() -> Result<()> {
    // ? 操作符自动转换错误
    let config = CaptureConfig::new(...)?;
    capture.start(config).await?;
    Ok(())
}

5.3 错误处理规则

场景使用原因
库 (Library)thiserror调用者需要精确处理不同错误
应用 (Application)anyhow简化错误传播,统一处理
Exampleanyhow演示代码,简洁优先
❌ 生产代码unwrap()禁止!可能导致 panic

6. Examples 编写规范

6.1 Example 位置

crates/embers-stream/
├── examples/
│   ├── capture_encode.rs    # 独立可运行
│   └── test_client.rs
└── src/

6.2 Example 结构模板

//! Example: 演示捕获和编码管道。
//!
//! # Usage
//!
//! ```bash
//! cargo run -p embers-stream --example capture_encode
//! ```

use embers_stream::{
    domain::{CaptureConfig, CaptureSource, FrameRate, Resolution},
    infra::{cgdisplay::CGDisplayCapture, encoder::GStreamerEncoder},
};
use futures::StreamExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 0. 初始化日志
    tracing_subscriber::fmt::init();

    // 1. 配置参数(带注释说明)
    let width = 1920u32;
    let height = 1080u32;
    let fps = 30u32;

    // 2. 创建组件
    let mut capture = CGDisplayCapture::new();
    let mut encoder = GStreamerEncoder::new(width, height, fps)?;

    // 3. 构建配置(展示 TypeDD 用法)
    let resolution = Resolution::new(width, height)
        .ok_or_else(|| anyhow::anyhow!("Invalid resolution"))?;
    let frame_rate = FrameRate::new(fps)
        .ok_or_else(|| anyhow::anyhow!("Invalid frame rate"))?;
    let config = CaptureConfig::new(resolution, frame_rate, PixelFormat::Bgra)?;

    // 4. 启动捕获
    capture.start(config).await?;

    // 5. 处理帧流
    let mut frame_stream = std::pin::pin!(capture.frame_stream());

    while let Some(frame) = frame_stream.next().await {
        encoder.encode(frame?)?;

        while let Some(packet) = encoder.pull_packets()? {
            // 处理编码后的包
        }
    }

    Ok(())
}

6.3 运行 Example

# 运行特定 example
cargo run -p embers-stream --example capture_encode

# 带参数运行
cargo run -p embers-stream --example capture_encode -- --help

# 带日志运行
RUST_LOG=debug cargo run -p embers-stream --example capture_encode

7. Tests 编写规范

7.1 测试位置

crates/embers-stream/
├── src/
│   ├── domain/
│   │   └── jitter.rs       // 内联单元测试 #[cfg(test)]
│   └── ...
└── tests/                   // 集成测试(可选)
    └── integration_test.rs

7.2 单元测试(内联)

// src/domain/jitter.rs

/// 抖动缓冲器。
pub struct JitterBuffer {
    // ...
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_frame(num: u64) -> (FrameNumber, Vec<u8>) {
        (FrameNumber::new(num).unwrap(), vec![num as u8; 100])
    }

    #[test]
    fn test_buffer_reorders_frames() {
        let mut buffer = JitterBuffer::new(3);

        // 乱序插入
        buffer.insert(make_frame(3));
        buffer.insert(make_frame(1));
        buffer.insert(make_frame(2));

        // 应该按顺序输出
        assert_eq!(buffer.pop().unwrap().0.get(), 1);
        assert_eq!(buffer.pop().unwrap().0.get(), 2);
        assert_eq!(buffer.pop().unwrap().0.get(), 3);
    }

    #[test]
    fn test_buffer_waits_for_target_depth() {
        let mut buffer = JitterBuffer::new(3);

        // 只插入 2 帧,不够 target_depth
        buffer.insert(make_frame(1));
        buffer.insert(make_frame(2));

        // 应该返回 None(等待更多帧)
        assert!(buffer.pop().is_none());
    }
}

7.3 异步测试

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::test;  // 注意:使用 tokio::test

    #[tokio::test]
    async fn test_async_capture() {
        let mut capture = MockCapture::new();
        capture.start(Default::default()).await.unwrap();

        let frame = capture.capture_frame().await.unwrap();
        assert!(!frame.data().is_empty());
    }
}

7.4 运行测试

# 运行所有测试
cargo test --workspace

# 运行特定 crate 测试
cargo test -p embers-stream

# 运行特定测试
cargo test -p embers-stream test_buffer_reorders

# 显示 println! 输出
cargo test -- --nocapture

# 运行被 ignore 的测试
cargo test -- --ignored

8. 文档注释规范

8.1 三要素

/// 抖动缓冲器,用于重排序网络传输的乱序帧。
///
/// # Why
///
/// 网络传输(尤其是 UDP/QUIC)可能乱序到达。
/// JitterBuffer 按 FrameNumber 排序,保证解码器收到连续帧。
///
/// # How
///
/// ```text
/// [网络] → [乱序帧: 3,1,2] → [JitterBuffer] → [有序帧: 1,2,3]
/// ```
///
/// # Example
///
/// ```
/// use embers_client::domain::JitterBuffer;
///
/// let mut buffer = JitterBuffer::new(3);
/// buffer.insert((frame_num, data));
/// if let Some(frame) = buffer.pop() {
///     // 处理帧
/// }
/// ```
pub struct JitterBuffer {
    // ...
}

8.2 模块级文档

//! 屏幕捕获领域抽象。
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────┐
//! │ CaptureSource   │  ← Trait (Domain)
//! └────────┬────────┘
//!          │ impl
//! ┌────────▼────────┐
//! │ CGDisplayCapture│  ← macOS 实现
//! │ X11Capture      │  ← Linux 实现
//! └─────────────────┘
//! ```
//!
//! # Trade-offs
//!
//! - **优点**: 平台无关的捕获逻辑
//! - **代价**: 需要运行时动态分发

pub use capture::{CaptureConfig, CaptureSource};

9. CI/CD 常用命令速查

9.1 本地开发

# 快速检查
cargo check --workspace

# 完整检查
cargo clippy --workspace --all-targets -- -D warnings

# 运行测试
cargo test --workspace

# 格式化
cargo fmt

# 构建文档
cargo doc --workspace --no-deps --open

9.2 CI Pipeline

# .github/workflows/ci.yml
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo fmt -- --check
      - run: cargo clippy --workspace --all-targets -- -D warnings
      - run: cargo test --workspace

9.3 发布前检查

# 1. 确保无警告
cargo clippy --workspace --all-targets -- -D warnings

# 2. 确保测试通过
cargo test --workspace

# 3. 检查文档
cargo doc --workspace --no-deps

# 4. 检查未使用的依赖
cargo machete  # 需要 cargo-machete

# 5. 安全审计
cargo audit    # 需要 cargo-audit

总结:Rust 工程化检查清单

检查项命令/规范
✅ Workspace 依赖统一[workspace.dependencies]
✅ 子 crate 继承version.workspace = true
✅ DDD 分层domain/ 无 IO,infra/ 实现 trait
✅ TypeDDNewType 模式,parse, don't validate
✅ 错误处理库用 thiserror,应用用 anyhow
✅ 无 unwrap生产代码禁止
✅ 文档注释What/Why/How 三要素
✅ Examplesexamples/ 目录,可独立运行
✅ Tests内联 #[cfg(test)] + 集成测试
✅ Clippy 无警告cargo clippy -- -D warnings
✅ 格式化cargo fmt