sgcProtoBuf: From .proto Files to Delphi Units | eSeGeCe Blog

sgcProtoBuf: From .proto Files to Delphi Units

· Components
sgcProtoBuf code generator

When a partner hands you a gRPC service, they hand you a folder of .proto files. Those files are the contract, and every message in them has to become something your Delphi code can read and write. Doing that by hand is tedious and easy to get wrong, and it has to be redone every time the other side ships a new version of the contract.

sgcProtoBuf is the code generator that does it for you. Point it at a .proto file and it produces Delphi units with a class for every message, ready to serialize and deserialize. It reads proto2 and proto3, and it takes the syntax from the file itself. It ships both as a wizard and as a command line tool.

Give it one file, get the whole tree

Real proto trees are never one file. They import each other, and they import the Google definitions on top of that. A typical service file starts like this:

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

With Resolve imports enabled, you select that one file and sgcProtoBuf follows the import statements recursively, generating every dependency it finds and adding the generated units to the uses clause. The root folder of your proto tree is discovered automatically from the file you picked, so there is usually nothing to configure.

If a definition cannot be found on disk, it is reported in the message list rather than quietly producing a unit that does not compile. You see exactly which file is missing instead of chasing a compiler error.

Unit names come from the package

The generated unit is named after the package statement, so package acme.telemetry.v1; produces unit acme.telemetry.v1; saved as acme.telemetry.v1.pas. The file name always matches the unit name, which removes a whole category of avoidable compile errors.

It also means several .proto files that declare the same package land in a single unit, exactly as the authors of the contract intended. A folder of device.proto, device_service.proto and units.proto all declaring acme.common.v1 becomes one acme.common.v1.pas.

The Google types are already in the box

Almost every real proto file imports something from google.protobuf. You do not need to generate those. sgcWebSockets already ships them in the sgcProtoBuf_WellKnownTypes unit, and the generator maps them automatically: Timestamp, Duration, Any, Empty, FieldMask, Struct and the wrapper types such as StringValue and Int32Value.

So the google.protobuf.Timestamp field above becomes a TsgcProtoBufTimestamp, with ToDateTime and FromDateTime already implemented. The Google .proto files never have to be translated at all.

Enums as real Delphi types

By default an enum is generated as a block of constants. Switch on Generate enums as Delphi enumerated types and you get a proper type instead, which is shorter to read and cannot be confused with an unrelated integer:

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

There is one honest limitation worth knowing. A Protocol Buffers enum can do things a Delphi enumerated type cannot: it can hold negative values, it can alias two names to the same number, and two enums in the same package can declare the same value name. When the generator meets one of those, it falls back to the constant form for that one enum and writes a comment saying why. Every other enum in the file is unaffected, so you get real types wherever they are possible and a working unit everywhere else.

The generated code

Every message class descends from a generated base class and exposes LoadFromBytes, LoadFromStream and ToBytes, with a property for each field. The example below uses the Keep original proto element names option, which drops the Tsgc prefix and keeps the names as they appear in the contract:

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;

On the command line

The wizard is convenient for a first look, but a proto tree changes, and regenerating it belongs in your build. The same generator runs from the command line:

sgcProtoBuf.exe --input=protos\acme\telemetry\v1\telemetry.proto --resolve-imports --output-dir=units --enum-as-type

The switches mirror the wizard options: --resolve-imports, --import-root (repeatable, for trees spread over more than one root), --output-dir, --enum-as-type, --original-names, --short-enum-consts, plus --no-classes, --no-services and --no-docs. Run it with --help for the full list.

Getting it

sgcProtoBuf ships with sgcWebSockets, alongside TsgcGRPCClient and the sgcProtoBuf runtime units. Download the latest build from the sgcWebSockets download page, and see the sgcProtoBuf topic in the help for the full option reference.

Questions, feedback or a proto tree that does not generate cleanly? Get in touch, you will get a reply from the people who wrote the code.