当合作方把一个 gRPC 服务交给你时,他们交给你的其实是一个装满 .proto 文件的文件夹。这些文件就是契约,其中的每一条消息都必须变成你的 Delphi 代码能够读写的东西。手工去做这件事既枯燥又容易出错,而且每当对方发布新版本的契约时,这些工作都得重来一遍。
sgcProtoBuf 就是替你完成这件事的代码生成器。把它指向一个 .proto 文件,它就会生成 Delphi 单元,为每条消息生成一个类,随时可以序列化和反序列化。它同时支持 proto2 和 proto3,并且直接从文件本身获取语法版本。它既以向导的形式提供,也以命令行工具的形式提供。
给它一个文件,得到整棵树
真实的 proto 树从来都不止一个文件。它们彼此 import,在此之上还会 import Google 的定义。一个典型的服务文件开头是这样的:
syntax = "proto3";
package acme.telemetry.v1;
import "acme/common/v1/device.proto";
import "google/protobuf/timestamp.proto";
message Reading {
string device_id = 1;
double value = 2;
google.protobuf.Timestamp taken_at = 3;
}
启用解析导入后,你只需选中那一个文件,sgcProtoBuf 就会递归跟踪 import 语句,生成它找到的每一个依赖,并把生成的单元加入 uses 子句。proto 树的根目录会根据你所选的文件自动发现,因此通常没有什么需要配置的。
如果某个定义在磁盘上找不到,它会被报告在消息列表中,而不是悄悄生成一个无法编译的单元。你能准确看到缺少的是哪个文件,不必再去追查编译器错误。
单元名来自 package
生成的单元以 package 语句命名,因此 package acme.telemetry.v1; 会生成 unit acme.telemetry.v1;,保存为 acme.telemetry.v1.pas。文件名始终与单元名一致,这消除了一整类本可避免的编译错误。
这也意味着,多个声明了同一个 package 的 .proto 文件会落到同一个单元里,正如契约的作者所设想的那样。一个文件夹中的 device.proto、device_service.proto 和 units.proto 若都声明 acme.common.v1,就会合并成一个 acme.common.v1.pas。
Google 类型已经内置
几乎每个真实的 proto 文件都会从 google.protobuf 导入一些东西。你不需要为它们生成代码。sgcWebSockets 已经在 sgcProtoBuf_WellKnownTypes 单元中提供了它们,生成器会自动完成映射:Timestamp、Duration、Any、Empty、FieldMask、Struct,以及诸如 StringValue 和 Int32Value 这样的包装类型。
因此上面那个 google.protobuf.Timestamp 字段会变成 TsgcProtoBufTimestamp,其中的 ToDateTime 和 FromDateTime 都已经实现好了。Google 的 .proto 文件完全不需要被翻译。
把枚举生成为真正的 Delphi 类型
默认情况下,枚举会生成为一组常量。打开将枚举生成为 Delphi 枚举类型,你得到的就是一个真正的类型,读起来更简短,也不会和某个无关的整数混淆:
CountryCodeDto = (
COUNTRY_CODE_DTO_NONE = 0,
COUNTRY_CODE_DTO_NL = 1,
COUNTRY_CODE_DTO_DE = 2,
COUNTRY_CODE_DTO_CH = 3
);
这里有一个值得如实说明的限制。Protocol Buffers 的枚举可以做一些 Delphi 枚举类型做不到的事:它可以保存负值,可以把两个名称指向同一个数字,同一个 package 中的两个枚举还可以声明相同的值名称。当生成器遇到这些情况时,它会针对那一个枚举回退到常量形式,并写下一条注释说明原因。文件中的其他枚举不受影响,因此在可能的地方你会得到真正的类型,在其余地方也照样得到一个可用的单元。
生成的代码
每个消息类都继承自一个生成的基类,并公开 LoadFromBytes、LoadFromStream 和 ToBytes,每个字段都有一个对应的属性。下面的示例使用了保留原始 proto 元素名称选项,它会去掉 Tsgc 前缀,让名称与契约中出现的形式保持一致:
uses
acme.telemetry.v1;
var
oReading: Reading;
begin
oReading := Reading.Create;
Try
oReading.LoadFromBytes(vBytes);
ShowMessage(oReading.DeviceId);
ShowMessage(FloatToStr(oReading.Value));
ShowMessage(DateTimeToStr(oReading.TakenAt.ToDateTime));
vBytes := oReading.ToBytes;
Finally
oReading.Free;
End;
end;
在命令行上
向导便于先看个大概,但 proto 树是会变化的,重新生成它应当属于你的构建流程。同一个生成器也可以从命令行运行:
sgcProtoBuf.exe --input=protos\acme\telemetry\v1\telemetry.proto --resolve-imports --output-dir=units --enum-as-type
这些开关与向导中的选项一一对应:--resolve-imports、--import-root(可重复使用,适用于分布在多个根目录下的树)、--output-dir、--enum-as-type、--original-names、--short-enum-consts,以及 --no-classes、--no-services 和 --no-docs。用 --help 运行即可查看完整列表。
如何获取
sgcProtoBuf 随 sgcWebSockets 一起提供,同行的还有 TsgcGRPCClient 和 sgcProtoBuf 运行时单元。请到 sgcWebSockets 下载页面下载最新版本,并查看帮助文档中的 sgcProtoBuf 主题,那里有完整的选项参考。
有问题、有反馈,或者遇到一棵无法顺利生成的 proto 树?联系我们,回复你的将是编写这些代码的人。
