protocプラグインとカスタムオプション

[以前の記事](https://qiita.com/yugui/items/87d00d77dee159e74886)ではprotocプラグインの書き方を紹介したが、実は1つ問題があった。
実用的なプラグインを書こうとした場合に、しばしば生成時に必要な、ドメイン固有の情報が足りないのである。本稿ではそれを補うカスタムオプションの話をする。

ここでもう一度確認しよう。protocのプラグインは[Protocol Buffersのスキーマ](https://qiita.com/yugui/items/160737021d25d761b353#protobuf)を読んで任意の処理を行える仕組みだ。それは`CodeGeneratorRequest`内の[`FileDescriptorProto` message](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto#L60)を読み取って任意のバイト列を出力し、出力を受け取った`protoc`が指定通りにファイルにバイト列を書き込んでくれる。

ただ、`FileDescriptorProto`はprotobufのスキーマ言語の文法をprotobufメッセージとして表現したものに過ぎないから、極めて一般的なデータ構造とサービス定義を表現する能力しか持たない。プログラミング言語依存の機能は意図的に除かれている。
でも、特定のシステム向けにスキーマを変換したり、特定の言語向けにスタブライブラリを生成したりするにはこうした一般的な情報では足りないのである。

自分のプラグインを書く場合も、例えばProtobufではサポートされていない型へのマッピングを指定するとか、サービスの認証設定を書くとか、何か固有の情報を付加してコード生成に反映させたい場合がありがちかと思う。

# オプション
ここで、生成対象に依存する情報を記述するためにoptionというprotobufの機能が必要になる。optionとはprotobufスキーマを構成する様々な要素に追加のメタデータを付与する仕組みだ。

たとえば、スキーマから生成されるGo用のスタブライブラリが属するpackage名を制御したいとする。当然のことながらprotobufスキーマ文法それ自体にはGo固有の機能は含まれていないから、次のようにoptionで指定することになる。

```proto
syntax = "proto3";
package example.protobuf;
option go_package = "example.com/example-package";

message SimpleMessage {
    uint64 id = 1;
    bytes blob = 4;
}
```

3行目でこのファイルに対する`go_package`というオプションを指定している。[Go用プラグイン`protoc-gen-go`](https://github.com/golang/protobuf/tree/master/protoc-gen-go)はこれを読み取り、生成されるコードに反映する。
`go_package`のようなオプションの値も`CodeGeneratorRequest`メッセージの中に入っているので、自分でプラグインを書くときはそれを適切なAPIで読み取って好きな処理をすれば良い。

## オプション指定の書き方
optionはファイル単位で指定できるのみではない。文法的には下記のものを対象にoptionを指定できる。

* ファイル
* メッセージ定義
* メッセージ定義内の各フィールド
* メッセージ定義内の各oneof定義
* 列挙型定義
* 列挙型定義の中の各列挙値
* サービス定義
* サービス定義内の各メソッド定義

上では文字列型のオプション`go_package`を使ったが、オプションはメッセージ型を含む任意のprotobufのデータ型を使えるので割とリッチな情報を載せられる。

### 例
例えば、[grpc-gatewayの場合](https://github.com/grpc-ecosystem/grpc-gateway/blob/d64f5319e5fab76565cc3be5c3ce8b18f931de5b/examples/examplepb/echo_service.proto#L26)を見てみよう。

```proto:echo_service.proto:L20-40
service EchoService {
	// Echo method receives a simple message and returns it.
	//
	// The message posted as the id parameter will also be
	// returned.
	rpc Echo(SimpleMessage) returns (SimpleMessage) {
		option (google.api.http) = {
			post: "/v1/example/echo/{id}"
			additional_bindings {
				get: "/v1/example/echo/{id}/{num}"
			}
		};
	}
	// (引用註:中略)
	//...
}
```

ここでは、`EchoService`というサービス定義内の`Echo`というメソッド定義にメッセージ型のオプション`google.api.http`を付与している。
付与されているのは[`google.api.HttpRule`メッセージ](https://github.com/grpc-ecosystem/grpc-gateway/blob/d64f5319e5fab76565cc3be5c3ce8b18f931de5b/third_party/googleapis/google/api/http.proto#L243)型の値で、[Protobufのテキスト形式](https://qiita.com/yugui/items/678a7abd86906270b5c2)で表記されている。

その他の文法要素に対するオプション指定例を以下に示す。
メッセージや列挙型のような`{ ... }`で括られた内部構造を持つ文法要素に対しては`option`キーワードを用いて指定し、それ以外の文法要素に対しては`[ ... ]`を後置して修飾していることが分かる。

```proto
syntax = "proto3";
package example.protobuf;

// ファイルに対するオプションjava_package
// 生成されるJava向けコードのパッケージ名を指定
option java_package = "com.example.protocplugin";

message SimpleMessage {
    // メッセージに対するオプションdeprecated。
    // 幾つかの生成先言語で、生成されるクラスに
    // @Deprecatedのようなアノテーションを付ける。
    option deprecated = true;

    message HeaderItem {
        string name = 1;
        string value = 2;
    }

    enum Type {
        // 列挙型に対するオプション。
        // 同じ値に対して複数のラベルの存在を許す。
        option allow_alias = true;

        START = 0 [deprecated = true]; // 列挙値に対するdeprecatedオプション
        BEGIN = 0;
        BLOB = 1;
        END = 2;
    }

    // フィールドに対するオプションjstype。
    // protobufのuint64型をJavaScriptのStringクラスと対応させる。
    uint64 id = 1 [jstype = JS_STRING];
    Type message_type = 2;
    repeated HeaderItem headers = 3;
    bytes blob = 4;
}

service Echo {
    // サービスに対するオプション
    option deprecated = true;

    rpc Echo(SimpleMessage) returns(SimpleMessage) {
        option idempotency_level = NO_SIDE_EFFECTS;
    }
}
```

(なんか例が`deprecated`オプションばかりになった。)

## オプションのスキーマ

一般にどの文法要素に対してどういう名前のオプションが利用可能かは、[descriptor.protoに書いてあるスキーマ](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto)を読めば分かる。各文法要素に対応して[`FileOptions`](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto#L297)とか、[`MessageOptions`](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto#L402)とかいうメッセージが定義されているので、それを見れば良い。これらの中の各々のフィールドが利用可能なオプションである。

例えば、`FileOptions`の定義を見てみると、次のようなフィールドがある。

```proto:descriptor.protoより
message FileOptions {
  ...
  // Generated classes can be optimized for speed or code size.
  enum OptimizeMode {
    SPEED = 1;        // Generate complete code for parsing, serialization,
                      // etc.
    CODE_SIZE = 2;    // Use ReflectionOps to implement these methods.
    LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
  }
  optional OptimizeMode optimize_for = 9 [default=SPEED];

  ...
}
```

よって、ファイルのトップレベルに`option optimize_for = CODE_SIZE;`とか書けて、(C++向けなどの)コード生成戦略を指定できることが分かる。

# カスタムオプションの定義の仕方

ちょっと待って欲しい。上では、利用可能なオプションが`descriptor.proto`の中のメッセージのフィールドとして定義されていると言った。確かに`go_package`だの`optimized_for`だの`deprecated`だのはそこに定義されているから良い。では、自分のアプリケーションに特化したoptionを定義するにはどうしたらよいのだろうか。
まさか、個別のアプリケーションのためにいちいちProtocol Buffersにプルリクエストを送って`descriptor.proto`を拡張してもらうわけにも行くまい。

ここで、extensionsというprotobufの仕組みが登場する。上の例で採り上げた`google.api.http`オプションも同じ仕組みで定義されている。extensionsで作ったオプションのことをカスタムオプションと呼んでいる。

## extensionsとは
extensionsとは、既存のメッセージ定義を外部から拡張してフィールドを足す仕組みだ。

これは少し分かりづらいので、まずは例を見てみたい。たとえば、[`EnumValueOptions`の定義](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto#L589)をよく見てみよう。

```proto:descriptor.protoより
message EnumValueOptions {
  // Is this enum value deprecated?
  // Depending on the target platform, this can emit Deprecated annotations
  // for the enum value, or it will be completely ignored; in the very least,
  // this is a formalization for deprecating enum values.
  optional bool deprecated = 1 [default=false];

  // The parser stores options it doesn't recognize here. See above.
  repeated UninterpretedOption uninterpreted_option = 999;

  // Clients can define custom options in extensions of this message. See above.
  extensions 1000 to max;
}
```

末尾に`extensions 1000 to max`と書いてある。これがextensionsの用意である。

ここで、protobufメッセージのフィールドは内部的にはフィールド名ではなく[タグ番号で識別される](https://developers.google.com/protocol-buffers/docs/proto3#assigning-tags)ということを思い出してもらいたい[^proto-tag]。`extensions 1000 to max`とはそのタグ番号に関する話で、`EnumValueOptions`のタグ番号のうち1000から最大値(536,870,911)までは第三者による拡張のために予約されていることを意味する。

さて、では拡張したい第三者としてはどうしたらよいのか。それには、自分の手元の適切な.protoファイルに次のように書けば良い[^extend-proto]。
5-7行目で`EnumValueOptions`を拡張し、タグ番号50000のフィールド`example.mypackage.description`を定義している。

```proto:custom-option.proto
syntax = "proto3";
package example.mypackage;
import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  string description = 50000;
}
```

これで、次のようにオプションを指定できる。

```proto:my-message.proto
syntax = "proto3";
package example.protobuf;

import "path/to/custom-option.proto";

message SimpleMessage {
    message HeaderItem {
        string name = 1;
        string value = 2;
    }

    enum Type {
        BEGIN = 0 [(example.mypackage.description) = "headersに値が入っている"];
        BLOB = 1  [(example.mypackage.description) = "blobにペイロードが入っている"];
        END = 2   [(example.mypackage.description) = "ストリームの終了"];
    }

    uint64 id = 1;
    Type message_type = 2;
    repeated HeaderItem headers = 3;
    bytes blob = 4;
}
```

## 注意事項
カスタムオプションやextensionsに関して幾つか注意事項があるので、ここで列挙しておく。

### カスタムオプションのタグ番号の私用領域
カスタムオプションを定義する場合、原則的には拡張フィールドのタグ番号として50000から99999を使うこと。

この範囲の番号は私的利用のために予約されているので[^private-tags]、外部とのタグ番号衝突を心配せずに自分たちの組織内で好きなように利用できる。

### 一意タグ番号の発行依頼
プラグインを広く一般に公開する場合、逆に私用領域のタグ番号を利用するのは望ましくない。そのプラグインのユーザーが自組織内で使っているプラグイン用のタグ番号と衝突するかもしれないからである。

そこでそういう場合には全世界で一意な番号を予約する必要がある。一意な番号は[Protobuf Global Extension Registry](https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md)というファイルで管理されているので、編集してGithub上でPull Requestを送ると程なく受理されるはずだ。

なお、少し前までは protobuf-global-extension-registry@google.com に申請メールを送る方式だった。このときの経験によればリクエストは数営業日ぐらいで処理されると思われる。

### メッセージ型の利用
上の例では文字列型のカスタムオプションを定義したが、これは実用上は避けるべきである。原則的にカスタムオプションはメッセージ型にした方が良い。

なぜならば、大抵の場合は後から2番目のカスタムオプションを足したくなるものだからだ。このとき、2つの理由から次のように2つのオプションを1つのメッセージに束ねた方が良い。

```proto
option (example.my_option) = {
  foo: 1
  bar: 2
};
```

理由その1は、このほうが関連するオプションがグループ化されて見やすいこと。下記のようにばらけているよりも、プラグインAで利用するためのオプションはその単位でまとまっていた方が良い。

```proto:グループ化されていない例
option (example.my_option_foo) = 1;
option (example.my_option_bar) = 2;
```

理由その2は、 タグ番号を節約するため。私用領域は限られているし、全世界で一意な番号はなおのこと希少だ。[^tag-saving]
その点、`foo`と`bar`という2つの設定を`example.my_option`という1つのメッセージ型オプションに束ねてしまえば、`(example.my_options)`用の番号が1個あれば済む。

```proto:番号の浪費
extend google.protobuf.MessageOptions {
  uint32 my_option_foo = 1022;
  uint32 my_option_bar = 1023;
  uint32 my_option_baz = 1024;
}
```

```proto:番号の節約
message MyOptions {
  uint32 foo = 10;
  uint32 bar = 11;
  uint32 baz = 12;
}

extend google.protobuf.MessageOptions {
  MyOptions my_option = 1022;
}
```

たとえ「このプラグイン用のカスタムオプションはbool型の1個で十分だろう」とそのとき確信していたとしても、将来に備えてメッセージ型のオプションを定義しておくべきである。

### extensionsとAny
extensionsは原理的には上のようにカスタムオプションを定義するためだけではなく、任意のメッセージを拡張できる仕組みである。
ただし、カスタムオプション以外の目的で新規に利用するのは推奨されていない。

Protobuf 3.0以降で一般のメッセージに対してそういうことをしたい場合、[Anyを使うべき](https://developers.google.com/protocol-buffers/docs/proto3#any)とされている。

# protocプラグインからの利用
先に言ったように、スキーマに書いたオプション指定はプラグインが受け取る`CodeGeneratorRequest`の中に入っている。だから後はリファレンスでも読んで好きなように処理すれば良い。
ただ、参考までにカスタムオプションをプラグインで処理する例をGoで紹介して締めくくろうと思う。

入力されたスキーマのうち`option (example.message_list) = {target: true}`というoptionを付けたメッセージだけ名前をリストアップするプラグインを作ってみようと思う。

## 資料の確認
繰り返しになるが、`CodeGeneratorRequest`のスキーマは[`plugin.proto`](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/compiler/plugin.proto)と[`descriptor.proto`](https://github.com/google/protobuf/blob/09354db1434859a31a3c81abebcc4018d42f2715/src/google/protobuf/descriptor.proto)を読めば分かる。このスキーマから生成されたGoのコードのAPIも知る必要があるが、それは[Go Generated Code](https://developers.google.com/protocol-buffers/docs/reference/go-generated)というドキュメントに書いてある。
それから、生成済みのGoコードが[`github.com/golang/protobuf/protoc-gen-go/descriptor`パッケージ](https://godoc.org/github.com/golang/protobuf/protoc-gen-go/descriptor)や[`github.com/golang/protobuf/protoc-gen-go/plugin`パッケージ](https://godoc.org/github.com/golang/protobuf/protoc-gen-go/plugin)にもあるのでこれを見ても良い。

他の言語の場合は下記を見る。

* [C++ Generated Code](https://developers.google.com/protocol-buffers/docs/reference/cpp-generated)
* [Java Generated Code](https://developers.google.com/protocol-buffers/docs/reference/java-generated)
* [Python Generated Code](https://developers.google.com/protocol-buffers/docs/reference/python-generated)
* [Ruby Generated Code](https://developers.google.com/protocol-buffers/docs/reference/ruby-generated)
* [C# Generated Code](https://developers.google.com/protocol-buffers/docs/reference/csharp-generated)
* [Objective C Generated Code](https://developers.google.com/protocol-buffers/docs/reference/objective-c-generated)
* [Javascript Generated Code](https://developers.google.com/protocol-buffers/docs/reference/javascript-generated)
* [PHP Generated Code](https://developers.google.com/protocol-buffers/docs/reference/php-generated)

## カスタムオプション定義
では、上記資料をもとに作り始めよう。今回のカスタムオプションはこんな感じになるだろう。

```proto:custom-options.proto
syntax = "proto3";
package example;
option go_package = "gist.github.com/e179eee28268e85c5036859987f8a15e";

import "google/protobuf/descriptor.proto";

message MessageListOptions {
	bool target = 10;
}

extend google.protobuf.MessageOptions {
	MessageListOptions message_list = 50000;
}
```

ここから、Goのコードを生成しておく。

```console
$ protoc --go_out=. -I. custom-options.proto
```

## プラグインの骨格
骨格は[以前の記事](https://qiita.com/yugui/items/87d00d77dee159e74886#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9)で作ったのと同じで良いはずだ。`processReq`関数だけ今回に合わせて改造する。

```go
func processReq(req *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse {
        files := make(map[string]*descriptor.FileDescriptorProto)
        for _, f := range req.ProtoFile {
                files[f.GetName()] = f
        }

        var buf bytes.Buffer
        for _, fname := range req.FileToGenerate {
                f := files[fname]
                for _, name := range listNames(f) {
                        io.WriteString(&buf, name)
                        io.WriteString(&buf, "\n")
                }
        }

        return &plugin.CodeGeneratorResponse{
                File: []*plugin.CodeGeneratorResponse_File{
                        {
                                Name:    proto.String("messages.txt"),
                                Content: proto.String(buf.String()),
                        },
                },
        }
}


func listNames(f *descriptor.FileDescriptorProto) []string {
	// TODO: ここに今回の処理を書く
}
```

入力は`CodeGeneratorRequest`メッセージで、`plugin.proto`を見れば`file`フィールドに`FileDescriptorProto`が入っていることが分かる。

```proto
message CodeGeneratorRequest {
  ...
  repeated FileDescriptorProto proto_file = 15;
  ...
}
```

`descriptor.proto`を見に行って`FileDescriptorProto`の定義を見ると、その中の`message_type`フィールドにファイル内のメッセージ定義が、更にその中の`options`フィールドにメッセージ定義に対するオプションが入っていることが分かる。

```proto
message FileDescriptorProto {
  ...
  repeated DescriptorProto message_type = 4;
  ...
}
```

```proto
message DescriptorProto {
  ...
  optional MessageOptions options = 7;
  ...
}
```

## コア部分の実装
[Go Generated Codeの記述](https://developers.google.com/protocol-buffers/docs/reference/go-generated#singular-message)のほうを見ると、`foo`というprotobufフィールドに対しては`GetFoo()`というgetterメソッドが定義されることが分かるので、次のようにしてみる。

```go
func listNames(file *descriptor.FileDescriptorProto) []string {
        var list []string
        for _, m := range file.MessageType {
                if !isTarget(m) {
                        continue
                }
                list = append(list, m.GetName())
        }
        return list
}
                                                                                    
func isTarget(m *descriptor.DescriptorProto) bool {
        var opts = m.GetOptions()
        // TODO: ここで判別する
}
```

さて、`google.protobuf.MessageOptions`メッセージのextensionsに我々のカスタムオプションが入っているのであった。なので、再び[ドキュメント](https://developers.google.com/protocol-buffers/docs/reference/go-generated#extensions)を見ると[`"github.com/golang/protobuf/proto".GetExtension`](https://godoc.org/github.com/golang/protobuf/proto#GetExtension)関数で取得できることが分かる。

第2引数に期待されている`ExtensionDesc`、すなわち`message_list`オプションの記述子は生成されたコードの中にあるんだろうなと当たりを付けられる。そこで先ほど生成した`custom-options.pb.go`の中を探すと`E_MessageList`があるのでこれを使う。

```go
func isTarget(m *descriptor.DescriptorProto) bool {
        var opts = m.GetOptions()
        if opts == nil {
                return false
        }

        ext, err := proto.GetExtension(opts, E_MessageList)
        if err == proto.ErrMissingExtension {
                return false
        }
        if err != nil {
                panic("unexpected error")
        }

        mopts := ext.(*MessageListOptions)
        return mopts.GetTarget()
}
```

完成したコードはhttps://gist.github.com/yugui/e179eee28268e85c5036859987f8a15e#file-main-go においてある。(若干エラー処理をサボっている)

# まとめ
* protocプラグイン開発の時、ターゲット固有の情報をスキーマに埋め込むためにカスタムのoptionを使う
* カスタムオプションを定義するには、`FileOptions`やその他の`descriptor.proto`に定義されたメッセージにextension fieldを作れば良い
  * このフィールドは、必ずmessage型で定義すべきである
  * 自組織内だけで流通するプラグインの場合は、私的領域50000-99999から一意なタグ番号を選んでフィールドを定義する
  * 外部に公開するプラグインを定義する場合、[Protobuf Global Extension Registry](https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md)にプルリクエストを送って全世界で一意な番号を取得する
* `plugin.proto`, `descriptor.proto`と、各言語別のガイドを読めばプラグイン内で必要になる情報を取得する方法は分かる。
* ここまでの記事により、読者は次のことができるようになった。
  * [Protocol Buffersの読みやすいスキーマ言語](https://qiita.com/yugui/items/160737021d25d761b353)でProtobuf自身やJSONを含む様々なデータ形式や通信, ストレージのスキーマを表現する。
  * カスタムオプションを定義し、自分のアプリケーションやライブラリに固有のメタデータをスキーマに盛り込む
  * [protocのプラグインを開発](https://qiita.com/yugui/items/87d00d77dee159e74886)し、protobufスキーマから他のスキーマ形式への変換や、スキーマからの情報抽出, その他スキーマの自動処理などをする
  * カスタムオプションの指定やProtobuf処理のデバッグに便利な[テキスト形式](https://qiita.com/yugui/items/678a7abd86906270b5c2#_reference-d07b5e3039911d78ed8b)を読み書きする


# 脚注
[^proto-tag]: https://qiita.com/yugui/items/160737021d25d761b353#protocol-buffers%E3%81%A8%E3%81%AF も参照
[^extend-proto]: 言い換えると、extensionsがあれば`EnumValuesOptions`に新たなフィールドを足すために`descriptor.proto`自体を編集する必要はない。だから自分が書き換える権限を持っていないファイルで定義されたメッセージを拡張できるというわけだ。
[^private-tags]: https://developers.google.com/protocol-buffers/docs/proto#customoptions
[^tag-saving]: それに1個オプションを足したいと思うたびに申請Pull Requestを送って返事を待つのもしんどいし、そもそもそういう無駄遣いは断られるような気もする。