sgcProtoBuf:从 .proto 文件到 Delphi 单元 | eSeGeCe 博客

sgcProtoBuf:从 .proto 文件到 Delphi 单元

· 组件
sgcProtoBuf 代码生成器

当合作方把一个 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.protodevice_service.protounits.proto 若都声明 acme.common.v1,就会合并成一个 acme.common.v1.pas

Google 类型已经内置

几乎每个真实的 proto 文件都会从 google.protobuf 导入一些东西。你不需要为它们生成代码。sgcWebSockets 已经在 sgcProtoBuf_WellKnownTypes 单元中提供了它们,生成器会自动完成映射:TimestampDurationAnyEmptyFieldMaskStruct,以及诸如 StringValueInt32Value 这样的包装类型。

因此上面那个 google.protobuf.Timestamp 字段会变成 TsgcProtoBufTimestamp,其中的 ToDateTimeFromDateTime 都已经实现好了。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 中的两个枚举还可以声明相同的值名称。当生成器遇到这些情况时,它会针对那一个枚举回退到常量形式,并写下一条注释说明原因。文件中的其他枚举不受影响,因此在可能的地方你会得到真正的类型,在其余地方也照样得到一个可用的单元。

生成的代码

每个消息类都继承自一个生成的基类,并公开 LoadFromBytesLoadFromStreamToBytes,每个字段都有一个对应的属性。下面的示例使用了保留原始 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 一起提供,同行的还有 TsgcGRPCClientsgcProtoBuf 运行时单元。请到 sgcWebSockets 下载页面下载最新版本,并查看帮助文档中的 sgcProtoBuf 主题,那里有完整的选项参考。

有问题、有反馈,或者遇到一棵无法顺利生成的 proto 树?联系我们,回复你的将是编写这些代码的人。