Edited at

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には登録されていない。一応、expireしたproposalはあり、application/protobufを提案していたようだ。

ちなみに、ググるとyuguiさんの記事がいっぱいヒットする。とても勉強になるのでみんな読もう。私はあと100回読むと思う。

今さらProtocol Buffersと、手に馴染む道具の話 - Qiita


バージョンについて

proto3とproto2があり、混在させることが可能。

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

syntax = "proto3";

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


ざっとsyntax

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


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でインストールできる。

$ brew install clang-format

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

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


import定義

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

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ならうまくやってくれるのだろうか?未確認)


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


googleが用意している型の例

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

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


repeated

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

message UserList { repeated User users = 1; }

初期値は空のリスト。


map

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

map<string, Project> projects = 3;

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


oneof

messageフィールドのある範囲が、どれか一つしか含まれないという状態を宣言することができる。

C言語でいう共用体のようなものか。構造上は平べったいままなので、タグ番号は一意なものを振っていく必要がある。

message SampleMessage {

oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}


service

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

service SearchService {

rpc Search (SearchRequest) returns (SearchResponse);
}


オプションについて

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