前面章节已经简单介绍了 tonic-build
的使用,本节将深入 tonic-build
,详细介绍在编译 .proto 文件时可提供的定制功能。
安装
cargo
使用 tonic-build
需要在 Cargo.toml
中配置以下依赖
1 | [dependencies] |
prost
提供了 protobuf 的支持,包括 protobuf 数据序列化,预定义的 google.protobuf.
数据类型等。tonic
提供了 gRPC 服务端/客户端支持,它基于 Axum 框架实现,可以复用 tower
生态提供的众多组件。
proto 文件
通常,proto 文件放在包(cargo 术语,可以理解成“项目”/“子项目”的意思)根目录下 proto
目录中,例如项目目录为 tonic-getting
,则 proto
目录结构如下:
1 | └── tonic-getting |
IDE 设置
VSCode
在 VSCode 中使用 rust-analyzer 时,启用 "rust-analyzer.cargo.buildScripts.enable": true
可以让 IDE 正确的识别生成的代码。你可以编辑 .vscode/settings.json
文件添加如下内容设置:
1 | { |
build.rs
cargo 提供了 build.rs
文件,用于在编译时执行自定义的构建脚本。如:链接到 C 库、生成代码等。先来看一个示例构建脚本,然后再来详细了解 tonic-build 提供的各个选项。
1 | use std::{env, path::PathBuf}; |
上面示例代码对部分常用选项进行了说明。tonic-build
提供了 Builder
类型,用于配置编译选项,后文对一些可能用到的重要选项进行说明。完整说明文档可以参考 tonic-build Builder 和 prost-build Config 。tonic 对 protobuf 消息的编译选项也由 prost-build
提供,查看代码的话会发现它内部调用了 prost-build
的 compile_protos
方法。
OUT_DIR
环境变量是 cargo 预定义的代码生成输出目录,从 .proto
编译的代码将生成到该目录中。
常用 Builder 选项说明
.file_descriptor_set_path
由 protoc
生成的 FileDescriptorSet
将写入此路径。这里注意,我们应该先获取 OUT_DIR
目录,再拼接文件名获得输出路径,不然文件将被写入到包根目录中。
.out_dir
设置输出目录以生成代码。默认为 OUT_DIR
环境变量指定目录,OUT_DIR
环境变量在编译时由 cargo 自动设置,因此通常不需要配置此选项。
.extern_path
声明外部提供的 protobuf 包或类型。例如,我们有 ultimate_api.page.Pagination
和 ultimate_api.Empty
类型,我们可以通过如下配置如它使用已定义的 ultimate_api::page::Pagination
和 ultimate_api::Empty
类型。
1 | .extern_path(".ultimate_api", "::ultimate_api"); |
这里需要注意的是,第一个参数指定 proto packapge
路径前缀时需要带 .
,例如 .ultimate_api
;第二个参数指定生成的 Rust 类型模块路径前缀,建议带 ::
来避免当前 crate
下有命名冲突。
.btree_map
.btree_map
有一个 paths
参数,指向特定字段、消息或包的路径。
后面的
.bytes
和几个.xxx_attribute
等选项的路径参数设置类似。
配置代码生成器为指定路径的字段且为 protobuf map
类型生成 BTreeMap 类型。这里的路径是一个路径前缀,既只要以此路径前缀匹配的字段都将生成 BTreeMap
类型。路径参数要以 .
开头,若只设置为 .
则表示所有 map
类型都成成为 BTreeMap
。
这里给出一些示例:
1 | // 匹配字段 |
.bytes
为 protobuf 的 bytes
类型生成 Rust bytes::Bytes
类型字段。需要添加 bytes
crate(cargo add bytes
)。
.type_attribute
为匹配的 message
、enum
和 oneof
添加额外属性。有两个参数:
paths
:P: AsRef<str>
的配置同上,也是一个前缀路径。attribute
:A: AsRef<str>
是要添加的属性,例如"#[derive(Eq)]"
。所有属性都是附加的,不会替换之前配置的任何属性,所以有可能触发编译器提示属性重复错误。
示例:
1 | // 为所有类型添加 `PartialEq` |
由于 oneof
字段在 protobuf 中没有自己的类型名称,因此字段名称可以同时与 type_attribute
和 field_attribute
一起使用。一个放在 enum
类型定义之前,另一个放在相应消息 struct
中字段之前。
.message_attribute
只向匹配的消息添加额外属性。
.enum_attribute
只向匹配的枚举添加额外属性。示例:
1 | // 为枚举添加 serde_repr,以匹配 Rust 的 repr 特性,以使用整形值(通常是 `i32`)进行序列化 |
.field_attribute
只向匹配的字段添加额外属性。
.protoc_arg
配置 protoc
的参数。例如,要启用 --experimental_allow_proto3_optional
参数。
.compile
方法(.compile(protos: &[impl AsRef<Path>], includes: &[impl AsRef<Path>]) -> Result<()>
)接受两个参数,protos
和 includes
,说明如下:
protos
:要编译的 proto 文件列表,任何间接导入的 .proto 文件都将自动包含在内。includes
:搜索导入的目录路径,目录按顺序搜索。传递给protos
(前一个参数)的.proto
文件必须在提供的包含目录之一中找到。
导入生成代码到项目
tonic
从 .proto 文件编译生成的 Rust 代码将输出到 OUT_DIR
目录(默认在 target/<debug/release>/build/<crate_name>-<hash>/out
目录),需要引入源码路径(src
目录内)才能编译到程序中。可以通过 tonic::include!
宏引入生成的代码。
1 | pub mod getting { |
这里引入了 3 个模块,每个模块都包含 .proto
文件中定义的 protobuf 消息类型。tonic-build
(内部调用prost-build
)会按 protobuf package
路径生成对于添加 .rs
后缀的 Rust 代码文件。
package getting;
(路径下有代码)将生成getting.rs
Rust 代码文件package getting.common;
(路径下有代码)将生成getting.common.rs
Rust 代码文件package getting.v1;
(路径下有代码)将生成getting.v1.rs
Rust 代码文件
tonic
生成的代码里面不会应用 protobufpackage
,也就是不会生成对应的 Rustmod
路径。我们需要自己定义 Rustmod
的层次关系,就像这里代码里的pub mod getting
和内部的pub mod common
以及pub mod v1
。
高级技巧
自行映射 prost 类型
prost 采用了宏来实现与 protobuf 数据的转换。因此,我们可以先定义 Rust struct
/enum
,而非先定义 .proto 消息再生成 Rust 代码。这在定义要在多个项目中复用的基础数据结构时很有用(比如 google.protobuf.
包下的消息就是这样定义的)。要使用这个功能,需要添加 prost
模块。
我们有一个 Pagination
类型,提供分页请求参数。它在很多 gRPC API 里都有使用,特别是在一些工具类,甚至数据库帮助方法里都有使用。那这样,由每个引入 .proto(比如:page.proto
)文件的项目都生成各自的 Rust 类型,这样是不利于复用的,而且也会在调用工具类和数据库帮助方法里多一次类型映射。因为 prost
通过 derive
宏来实现对 protobuf 的二进制序列化,我们可以定义的消息。
1 | use serde::{Deserialize, Serialize}; |
完整代码见: https://github.com/yangbajing/ultimate-common/blob/main/crates/ultimate-api/src/v1/page.rs
在 struct
的 derive
上添加 Clone, PartialEq, ::prost::Message
以支持 protobuf 二进制序列化。其它的宏可以根据项目需要自行添加。在 build.rs
里配置 .extern_path(".ultimate_api", "::ultimate_api");
后,tonic-build
就不会生成相应的 Rust 类型,而是直接使用已存在的 ::ultimate_api
路径开头的类型。
在字段上通过 prost
宏设置对应 protobuf 的字段类型、字段编号、标记修饰(如:repeated
、optional
)。
小结
本文讨论了如何使用 tonic-build
生成 gRPC 服务的 Rust 代码,以及如何使用 prost
生成自定义类型。tonic
提供了丰富的配置选项,可以让我们控制生成代码的方式,如:添加自定义属性、自定义类型、是否生成服务端/客户端代码等。