sgcProtoBuf: .proto ファイルから Delphi ユニットへ | eSeGeCe ブログ

sgcProtoBuf: .proto ファイルから Delphi ユニットへ

· コンポーネント
sgcProtoBuf コードジェネレーター

パートナーから gRPC サービスを受け取るとき、実際に渡されるのは .proto ファイルの入ったフォルダーです。それらのファイルが契約であり、その中のすべてのメッセージを Delphi のコードで読み書きできる形にしなければなりません。これを手作業で行うのは手間がかかるうえに間違えやすく、しかも相手側が契約の新しいバージョンを出すたびにやり直す必要があります。

sgcProtoBuf は、その作業を代わりに行うコードジェネレーターです。.proto ファイルを指定するだけで、メッセージごとにクラスを備えた Delphi ユニットが生成され、そのままシリアライズとデシリアライズを行えます。proto2 と proto3 の両方を読み取り、構文はファイル自体から判断します。ウィザードとコマンドラインツールの両方が提供されます。

ファイルを 1 つ渡せば、ツリー全体が手に入ります

実際の proto ツリーが 1 ファイルで済むことはありません。互いに import し合い、さらにその上に Google の定義を import します。典型的なサービスファイルは次のように始まります。

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;
}

インポートを解決する を有効にしてそのファイルを 1 つ選択すると、sgcProtoBuf は import 文を再帰的にたどり、見つかったすべての依存関係を生成して、生成したユニットを uses 節に追加します。proto ツリーのルートフォルダーは選択したファイルから自動的に検出されるため、通常は設定するものは何もありません。

定義がディスク上に見つからない場合は、コンパイルできないユニットを黙って生成するのではなく、メッセージリストに報告されます。コンパイラーのエラーを追いかける代わりに、どのファイルが不足しているのかが正確にわかります。

ユニット名は package から決まります

生成されるユニットは package 文にもとづいて命名されます。つまり package acme.telemetry.v1; からは unit acme.telemetry.v1; が生成され、acme.telemetry.v1.pas として保存されます。ファイル名は常にユニット名と一致するため、避けられるはずのコンパイルエラーが 1 種類まるごとなくなります。

これは同時に、同じ package を宣言する複数の .proto ファイルが 1 つのユニットにまとまることも意味します。まさに契約の作成者が意図したとおりです。acme.common.v1 を宣言する device.protodevice_service.protounits.proto が入ったフォルダーは、1 つの acme.common.v1.pas になります。

Google の型は最初から同梱されています

実際の proto ファイルのほとんどは google.protobuf から何かを import します。それらを生成する必要はありません。sgcWebSockets はすでに sgcProtoBuf_WellKnownTypes ユニットでそれらを提供しており、ジェネレーターが自動的にマッピングします。対象は TimestampDurationAnyEmptyFieldMaskStruct、そして StringValueInt32Value といったラッパー型です。

そのため、上記の google.protobuf.Timestamp フィールドは TsgcProtoBufTimestamp になり、ToDateTimeFromDateTime がすでに実装されています。Google の .proto ファイルを変換する必要はまったくありません。

enum を本物の Delphi 型として

既定では enum は定数のブロックとして生成されます。enum を Delphi の列挙型として生成する を有効にすると、代わりに正式な型が得られます。読み取りが短く済み、無関係な整数と取り違えることもありません。

CountryCodeDto = (
  COUNTRY_CODE_DTO_NONE = 0,
  COUNTRY_CODE_DTO_NL = 1,
  COUNTRY_CODE_DTO_DE = 2,
  COUNTRY_CODE_DTO_CH = 3
);

ここには、知っておく価値のある正直な制限が 1 つあります。Protocol Buffers の enum には、Delphi の列挙型にはできないことができます。負の値を保持でき、2 つの名前を同じ番号にエイリアスでき、同じ package 内の 2 つの enum が同じ値名を宣言できます。ジェネレーターがそうしたケースに出会うと、その enum についてのみ定数形式にフォールバックし、その理由を説明するコメントを書き込みます。ファイル内の他の enum は影響を受けないため、可能な箇所では本物の型が得られ、それ以外の箇所でも動作するユニットが得られます。

生成されるコード

すべてのメッセージクラスは、生成された基底クラスを継承し、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 は、TsgcGRPCClientsgcProtoBuf ランタイムユニットとともに sgcWebSockets に同梱されています。最新のビルドは sgcWebSockets のダウンロードページから入手できます。オプションの完全なリファレンスについては、ヘルプの sgcProtoBuf トピックをご覧ください。

ご質問やフィードバック、あるいはうまく生成できない proto ツリーはありませんか。お問い合わせください。コードを書いた本人から返信が届きます。