前面章节已经简单介绍了 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.rsRust 代码文件package getting.common;(路径下有代码)将生成getting.common.rsRust 代码文件package getting.v1;(路径下有代码)将生成getting.v1.rsRust 代码文件
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 提供了丰富的配置选项,可以让我们控制生成代码的方式,如:添加自定义属性、自定义类型、是否生成服务端/客户端代码等。