如何定义 proto 并生成 Rust 代码?
解读
在国内 Rust 服务端面试中,gRPC 与 Protobuf 的组合几乎是“云原生微服务”标配。面试官问“如何定义 proto 并生成 Rust 代码”,表面看是流程题,实则考察三点:
- 是否熟悉 prost / tonic 这一国内主流方案(Google 官方 rust-protobuf 已边缘化);
- 能否把 Cargo 构建脚本(build.rs) 与 prost-build/tonic-build 无缝集成,实现“一次编译,终身同步”;
- 是否理解 prost 的字段类型映射、包名到模块名的转换规则,以及 no_std / wasm32 场景下的裁剪技巧。
答不出“build.rs 怎么写”或“为什么用 tonic 而不是 grpc-rs”,会被直接判定为“只写过玩具项目”。
知识点
- prost:Protobuf 纯 Rust 解析与序列化库,无 C++ 依赖,在国产化信创环境(麒麟、统信 UOS)下可静态编译。
- tonic:基于 prost 的 gRPC 实现,支持 HTTP/2、流式调用、Tower 中间件,与 Kubernetes 生态无缝衔接。
- build.rs:Cargo 编译钩子,先于 main.rs 执行,用于调用 prost-build/tonic-build 生成 *.rs 文件,避免“手工 protoc”带来的 CI 漂移。
- 类型映射:proto3 的
int64→i64,string→String,bytes→Vec<u8>;timestamp 用 prost_types::Timestamp,需显式依赖prost-types。 - 属性注入:通过
#[derive(serde::Serialize, Deserialize)]让生成的结构体同时支持 JSON,国内前后端联调必备。 - no_std 裁剪:在
Cargo.toml中关闭std 特性,并用prost = { default-features = false, features = ["prost-derive"] }实现嵌入式 WASM 体积最小化。
答案
- 目录布局(国内大厂惯用 mono-repo)
proto/
shop/
v1/
order.proto
src/
main.rs
build.rs
Cargo.toml
- 定义 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;
}
- 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"
- 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(())
}
- 使用生成的代码
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(())
}
- 常用调优
- 字段重命名:
.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% 编译时间。
拓展思考
- 与 grpc-rs 对比:grpc-rs 基于 C++ core,在信创 ARM64 平台需自行编译 boringssl,而 tonic 纯 Rust,可交叉编译到 openEuler RISC-V。
- streaming 背压:tonic 基于 Tower Service,可用
ConcurrencyLimit中间件 实现国内“双十一”场景下的限流熔断,面试可展开“如何自定义 Layer”。 - WASM 双向流:浏览器前端用
tonic-web+wasm-bindgen,把 grpc-web 帧转成 HTTP/1.1,实现“Rust 写前端+后端同一套 proto”,在 2024 国内低延迟互动直播方案中已成标配。