如何定义 proto 并生成 Rust 代码?

解读

在国内 Rust 服务端面试中,gRPC 与 Protobuf 的组合几乎是“云原生微服务”标配。面试官问“如何定义 proto 并生成 Rust 代码”,表面看是流程题,实则考察三点:

  1. 是否熟悉 prost / tonic 这一国内主流方案(Google 官方 rust-protobuf 已边缘化);
  2. 能否把 Cargo 构建脚本(build.rs)prost-build/tonic-build 无缝集成,实现“一次编译,终身同步”;
  3. 是否理解 prost 的字段类型映射、包名到模块名的转换规则,以及 no_std / wasm32 场景下的裁剪技巧。

答不出“build.rs 怎么写”或“为什么用 tonic 而不是 grpc-rs”,会被直接判定为“只写过玩具项目”。

知识点

  1. prost:Protobuf 纯 Rust 解析与序列化库,无 C++ 依赖,在国产化信创环境(麒麟、统信 UOS)下可静态编译。
  2. tonic:基于 prost 的 gRPC 实现,支持 HTTP/2、流式调用、Tower 中间件,与 Kubernetes 生态无缝衔接。
  3. build.rs:Cargo 编译钩子,先于 main.rs 执行,用于调用 prost-build/tonic-build 生成 *.rs 文件,避免“手工 protoc”带来的 CI 漂移。
  4. 类型映射:proto3 的 int64i64stringStringbytesVec<u8>timestamp 用 prost_types::Timestamp,需显式依赖 prost-types
  5. 属性注入:通过 #[derive(serde::Serialize, Deserialize)] 让生成的结构体同时支持 JSON,国内前后端联调必备。
  6. no_std 裁剪:在 Cargo.toml 中关闭 std 特性,并用 prost = { default-features = false, features = ["prost-derive"] } 实现嵌入式 WASM 体积最小化

答案

  1. 目录布局(国内大厂惯用 mono-repo)
proto/
  shop/
    v1/
      order.proto
src/
  main.rs
build.rs
Cargo.toml
  1. 定义 proto(包名与模块名严格对齐,避免 prost 生成路径错乱)
syntax = "proto3";
package shop.v1;

message Order {
  string order_id = 1;
  int64  created_at = 2;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
}

message CreateOrderRequest {
  Order order = 1;
}
message CreateOrderResponse {
  string order_id = 1;
}
  1. Cargo.toml(** tonic 0.11 + prost 0.12 为 2024 国内稳定组合 **)
[dependencies]
tonic = "0.11"
prost = "0.12"
tokio = { version = "1", features = ["full"] }

[build-dependencies]
tonic-build = "0.11"
  1. build.rs(**关键步骤,CI 可缓存 OUT_DIR **)
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .build_client(true)
        .build_server(true)
        .out_dir("src/pb")          // 生成到版本控制外,避免冲突
        .compile(&["proto/shop/v1/order.proto"], &["proto"])?;
    Ok(())
}
  1. 使用生成的代码
pub mod pb {
    tonic::include_proto!("shop.v1");   // 与 package 名字一致
}

use pb::{order_service_client::OrderServiceClient, CreateOrderRequest, Order};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = OrderServiceClient::connect("http://[::1]:50051").await?;
    let request = tonic::Request::new(CreateOrderRequest {
        order: Some(Order {
            order_id: "123".to_string(),
            created_at: chrono::Utc::now().timestamp(),
        }),
    });
    let response = client.create_order(request).await?;
    println!("ORDER_ID={}", response.into_inner().order_id);
    Ok(())
}
  1. 常用调优
  • 字段重命名.type_attribute(".shop.v1.Order", "#[derive(serde::Serialize)]")
  • JSON 兼容prost_build::Config::new().btree_map(["."]) 把 map 生成 BTreeMap,前端顺序可控。
  • CI 缓存:把 OUT_DIR 指向 $CARGO_TARGET_DIR/proto-gen,配合 Swatinem/rust-cache@v2 减少 30% 编译时间。

拓展思考

  1. 与 grpc-rs 对比:grpc-rs 基于 C++ core,在信创 ARM64 平台需自行编译 boringssl,而 tonic 纯 Rust,可交叉编译到 openEuler RISC-V
  2. streaming 背压:tonic 基于 Tower Service,可用 ConcurrencyLimit 中间件 实现国内“双十一”场景下的限流熔断,面试可展开“如何自定义 Layer”。
  3. WASM 双向流:浏览器前端用 tonic-web+wasm-bindgen把 grpc-web 帧转成 HTTP/1.1,实现“Rust 写前端+后端同一套 proto”,在 2024 国内低延迟互动直播方案中已成标配。