protocol buffers IDL再入門

gRPC関連でprotocol buffersを使うことが増えたのだが、protocol buffers自体にまだ慣れていないので調べてまとめる。
https://developers.google.com/protocol-buffers/docs/proto3

## protocol buffers自体のおさらい

Googleが2008年にOSS化したプログラミング言語に中立なデータのシリアライズ形式である。(略称はprotobuf)
型情報は`.proto`というIDLに書き、これを送信と受信の双方で事前共有しておく。こうすることによって余計な型情報を省いてバイナリに固めることができる。
この性質から、APIドキュメントとしても使える。「RESTっぽいJSON API + OpenAPIによる記述」とだいたい同じニーズをカバーする。(IDLを抜きにしてMessagePackやJSONと比較することはできない)

protobufを使った開発の順序はこんな感じ。

1. `.proto`を書く (お好きなエディタをお使いください)
2. `protoc`コマンドで各言語用のシリアライズ/アンシリアライズのコードを吐き出す
3. 何らかの通信手段でシリアライズしたものをやり取りする

シリアライズしたものはバイナリフォーマット。HTTP上にprotobufのバイナリを流すことも可能だが、残念ながら[IANAのMedia Types](https://www.iana.org/assignments/media-types/media-types.xhtml)には登録されていない。一応、expireしたproposalはあり、[application/protobuf](https://tools.ietf.org/html/draft-rfernando-protocol-buffers-00)を提案していたようだ。

ちなみに、ググるとyuguiさんの記事がいっぱいヒットする。とても勉強になるのでみんな読もう。私はあと100回読むと思う。
[今さらProtocol Buffersと、手に馴染む道具の話 - Qiita](https://qiita.com/yugui/items/160737021d25d761b353)

## バージョンについて

proto3とproto2があり、混在させることが可能。
ただ、互換性問題などを考えた上でproto3が開発されたようなので、proto3を使っていけばいいと思う。機能自体もproto3の方が少なく、覚えることも少ない。この記事でも極力proto3のことだけを書く。

```protobuf
syntax = "proto3";
```

これをファイルの頭に書いておくとproto3として認識される。

## ざっとsyntax

拡張子は`.proto`を使う。勉強のために作ったprotoがここにあるので、ビルドまで試したい場合は参考にしてほしい。 https://github.com/hirak/protobuf-test/

```protobuf:user.proto
syntax = "proto3";

package myapp;

import "google/api/annotations.proto";
import "google/type/date.proto";
import "google/protobuf/empty.proto";

message User {
  uint64 id = 1;
  string first_name = 2;
  string family_name = 3;
  Sex sex = 4;
  uint32 age = 5 [ deprecated = true ];
  google.type.Date birthday = 6;

  enum Sex {
    SEX_UNKNOWN = 0;
    MALE = 1;
    FEMALE = 2;
    OTHER = 3;
  }
}

message UserList { repeated User users = 1; }

service UserService {
  rpc Get(GetRequest) returns (User) {
    option deprecated = false;
    option (google.api.http) = {
      get : "user"
    };
  }
  rpc List(google.protobuf.Empty) returns (UserList) {}
}

message GetRequest { uint64 id = 1; }

```

主に、`message`という構造体のようなものを定義していく。
更に、`service`という定義を作ると、serverおよびclientの実装を吐き出してくれるprotocプラグインもある。(対応するプラグインを使わなければ無視されるだけ)


### インデントを綺麗にする

clang-formatがおすすめ。https://clang.llvm.org/docs/ClangFormat.html
macOSならHomebrewでインストールできる。

```bash
$ brew install clang-format
```

`-i`でインデントし直して上書きする。

```bash
$ clang-format -i path/to/xxx.proto
```

### import定義

別のファイルに書いてある定義を読み込むことができる。

```protobuf
import "google/api/annotations.proto";
import "google/type/date.proto";
import "google/protobuf/empty.proto";
```

冒頭のサンプルだと、 `google.protobuf.Empty`, `google.type.Date`, `google.api.http` はどこにも定義されていないが、これらの外部ファイルから読み込むことでコンパイルを通している。

importのファイルは、何もオプションを指定しなければカレントディレクトリから探され、 `-IPATH`か`--proto_path=PATH`で探索ディレクトリを複数指定することができる。

この辺の仕様は若干古めかしい。。まあ2008年の仕組みだからね。`protoc`コマンド単体でビルドを組むのは無理ゲーなので、Makefileなどを組み合わせることになると思う。

なお、よく登場するこの辺のprotoは色々なリポジトリに点在している。git submoduleで寄せ集めてくる場合はimport pathを合わせるのが割と面倒くさい…。（bazelならうまくやってくれるのだろうか？未確認）

* google/api/xxx.proto → https://github.com/googleapis/googleapis/tree/master/google/api
* google/type/xxx.proto → https://github.com/googleapis/googleapis/tree/master/google/type
* google/protobuf/xxx.proto → https://github.com/protocolbuffers/protobuf/tree/master/src/google/protobuf

## protobufの型

### 組み込みのスカラ型

スカラ型は一通り組み込まれている。

* bool
* string
* bytes
* double, float
* int32, int64
* uint32, uint64
* sint32, sint64
    * 負数を効率よくシリアライズする
* fixed32, fixed64
    * 巨大な数を効率よくシリアライズする
* sfixed32, sfixed64


### ユーザー定義型

`message`というキーワードで構造体のようなものを定義することができる。protocでコンパイルすると、大抵は言語のクラスや構造体としてコードが生成される。スカラ型や、他のユーザー定義の型を集約して構造を作っていく。

### enum

列挙型も作れる。
なお、enumの最初の値がデフォルト値のゼロでなければならない。これはproto3の決まりなので必ず守ろう。特に使いみちがない場合は`UNKNOWN = 0`などを入れておくこと。
値に使えるのは32bit 整数の範囲のみ。

こちらにもまとめた。
[protocol buffers ENUM型 完全ガイド - Qiita](https://qiita.com/Hiraku/items/4cebcce22e38c644298b)


### googleが用意している型の例

メジャーな概念であれば、大抵googleのリポジトリに存在していたりする。

* [google.protobuf.Empty](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto) ... 空オブジェクト。JSONにすると`{}`になる。
* [google.protobuf.Timestamp](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto) ... ナノ秒まで保持できる時刻型。JSONにすると`"1996-10-30T12:08:25Z"`という文字列になる。timezoneは保持しない。
* [google.protobuf.Duration](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/duration.proto) ... 時刻じゃなくて時間。ナノ秒単位まで保存できる。JSONにすると`"3s"`というような秒数を表す文字列になる。
* [google.type.Date](https://github.com/googleapis/googleapis/blob/master/google/type/date.proto) ... 単純にY-M-Dの3点セットを保持する日付型。timezoneは持たない。生年月日に便利。
* [google.type.TimeOfDay](https://github.com/googleapis/googleapis/blob/master/google/type/timeofday.proto) ... 単純にhours,minutes,seconds,nanosの4点セットを保持する時刻型。Timestampと違って日付を持たない。
* [google.type.DayOfWeek](https://github.com/googleapis/googleapis/blob/master/google/type/dayofweek.proto) ... 曜日のenum。
* [google.type.Money](https://github.com/googleapis/googleapis/blob/master/google/type/money.proto) ... currency_codeを含む通貨型。
* [google.type.LatLng](https://github.com/googleapis/googleapis/blob/master/google/type/latlng.proto) ... 緯度経度
* [google.type.PostalAddress](https://github.com/googleapis/googleapis/blob/master/google/type/postal_address.proto) ... 住所
* [google.type.Color](https://github.com/googleapis/googleapis/blob/master/google/type/color.proto) ... RGBAで表す色の型

他にもgoogleapis/googleapisには色々な型が定義されているので、読むと勉強になる。


### repeated

型の前に「repeated」というキーワードを書くと、配列のように複数個含められるようになる。

```protobuf
message UserList { repeated User users = 1; }
```

初期値は空のリスト。

### map

連想配列のようなものも作ることができる。ただし、keyになりうるのはstringとint系、boolだけである。enumや独自のmessage型、floatやbytesはkeyにできない。

```protobuf
map<string, Project> projects = 3;
```

mapとrepeatedを混ぜることはできない。

### oneof

messageフィールドのある範囲が、どれか一つしか含まれないという状態を宣言することができる。
C言語でいう共用体のようなものか。構造上は平べったいままなので、タグ番号は一意なものを振っていく必要がある。

```protobuf
message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
```

## service

RPCを定義する構文。protocプラグインに、これを認識するものがなければ無視される。

```protobuf
service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}
```

## オプションについて

(力尽きたので別の記事にします)