protobuf v3 の optional について

こんにちは。テクノロジー本部バックエンド開発グループの山田です。 この記事は キャディ Advent Calendar 2020 の8日目です。前日は桐生さんの tailwindcss のコンセプトとメリットについての考察 でした。

今回は以前のgRPCの記事に引き続き、システム開発の BFF や BE 間の通信で利用している gRPC & protocol buffers (以下 protobuf ) に関する記事です。 開発/運用していく中で protobuf 上での Optional な値の扱いをどうするかという点で何度も困らせられた苦い思い出をもとに、 proto3 で実験的に復活した optional について調査・検証していきます。

検証に利用したコードはこちらにあります。

時間のない方は最下部のまとめ欄をご覧ください。


[toc]

optionalの定義方法と実体

久しぶりに protobuf と戯れることにしたため、まずは公式から情報を得ることとします。 公式に記述されている通り、v3.12.0より実験的機能として復活し、内部的には oneof のフィールドとして扱われる実装となっているようです。

optionalの検証

実際に触りながら差分を確認していくために、今回は以下のようなprotoの定義を利用して、検証していきます。

syntax = "proto3";

package pingpong;

service PingPong {
  rpc SendPing(Ping) returns (Pong) {}
}

message Ping {
  string type = 1;
  optional string payload = 2;
}

message Pong {
  optional string payload = 1;
}

今回はキャディのシステム開発で利用している Rust / JS / TS に関して検証することとします。 まずRustでの対応状況を確認していきます。

Rust

Option に変換されていい感じに使えるのではないか、と期待していたものの、protobuf の有名所 crate である stepancheg/rust-protobufdanburkert/prost はどちらも issue がたてられているだけで、まだ実装されていませんでした。

■ 該当 issue

rust-protobuf は version 3 に組み込まれる予定になっているようなので、動向が気になるところです。 rust-protobuf version 3 · Issue #518 · stepancheg/rust-protobuf · GitHub

出落ちのような形で残念ですが、続いて JS / TS での状況を確認してみます。

JavaScript / TypeScript

あらかじめ、JS / TS の検証で protoc の呼び出しをするために利用している grpc-tools を導入しておきます。 ※ yarn 及び typescript を導入済みであることを前提にしています

yarn add -D grpc-tools

以前のgRPCの記事でもまとめましたが、 protobufの扱い方はいくつかあるため、複数のライブラリで検証をすすめてみます。

protoc の JS 出力を使う方法

もともと提供されている、protoc の JS 出力を利用することで生成はできました。 しかし残念ながら 出力された JS に関連する TS 向けの型定義ファイルの出力を行う protoc のプラグインは対応されているものを見つけることはできませんでした。 そのため、今回は protoc で生成された JS の確認のみにとどめます。

以下は生成するためのコマンドと、生成された protobuf 向けコードの抜粋です。 生成コードを確認してみると、今回 optional で定義した payload というフィールドに対して、以下のように oneof と同等のコードが生成されていました。

■ ライブラリ導入

yarn add google-protobuf

■ コード生成 shell の一部

DEST_DIR=./generated
"$(yarn bin)"/grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${DEST_DIR} \
--experimental_allow_proto3_optional \
-I ../ ../pingpong.proto

■ 生成されたコードの一部

/**
 * Clears the field making it undefined.
 * @return {!proto.pingpong.Ping} returns this
 */
proto.pingpong.Ping.prototype.clearPayload = function() {
  return jspb.Message.setField(this, 2, undefined);
};

/**
 * Returns whether this field is set.
 * @return {boolean}
 */
proto.pingpong.Ping.prototype.hasPayload = function() {
  return jspb.Message.getField(this, 2) != null;
};

各型での optional については、公式のテストコード (protobuf/proto3_test.js) にて網羅的に記述されていたので確認してみてください。

続いて protoc の JS 出力を使わない方法 の検証をしていきます。

protocの JS 出力以外の方法

キャディの開発では以前検証した情報をもとに protobuf.js を利用しているため、リポジトリの Issue や PR を覗いてみましたが、残念なことに特に動きがないことがわかりました。

■関連 issue

しかし、上記 Issue にリンクされている Issue をたどってみると、stephenh/ts-proto というレポジトリたどりつきました。

たどりついた Issue の中には、なんと 2020年9月に公開された v1.32.0 にて optional に対応したと書かれています。 (対応しているライブラリがほぼ見つけられていない状態だったので、この記事のコンセプトが崩れずにすんでよかったです)

ということで、ここから ts-proto で optional の検証をしていきます。

ts-proto の README 等をみてみると、proto ファイルをもとに protobuf.js の encode / decode を利用するようにラップされたコードを生成してくれる protoc のプラグインだということがわかりました。

ここから検証コードを作って生成後のコードをみていきます。

■ ライブラリ導入

yarn add -D ts-proto
yarn add protobufjs

■ コード生成 shell の一部

DEST_DIR=./generated
"$(yarn bin)"/grpc_tools_node_protoc \
--plugin="$(yarn bin)/protoc-gen-ts_proto" \
--ts_proto_out="${DEST_DIR}" \
--ts_proto_opt="lowerCaseServiceMethods=true" \
--experimental_allow_proto3_optional \
-I ../ ../pingpong.proto

■ 生成されたコードの一部

export interface Ping {
  type: string;
  payload?: string | undefined;
}

export interface Pong {
  payload?: string | undefined;
}

生成されたコードでは、TS 上で Optional フィールドとして定義されており、oneof とは異なる定義となっています。 ( oneof を利用する場合には union 型で出力するオプションなどがありました)

ts-proto の考察

少し脱線しますが、上記確認をするなかで ts-proto が非常に便利そうだったため、今後の開発の検討のために良い点と気になる点をまとめておくことにします。

良い点

  • optional の対応
  • protoc の js 出力でできなく、protobuf.js には定義されていたオブジェクトからの代入と同等の実装がある(fromObject)
  • oneof を union 型で出力できる
  • gRPC のサービス実装で注目されている Twirp, grpc-web, NestJS 向けの実装がある
  • 内部の encode / decode に protobuf.js の処理を利用してるため高速(protobuf.jsのベンチマークを信じると)

気になる点

  • 出力されるコードの内部に any が利用されている
  • message に対応するモデルの定義が、 同名の interface と const で指定されており、型が少し扱いにくい
  • grpc-loader 等と組み合わせて利用する場合に、 Service の定義がシンプル(Promiseを利用した引数一つの定義しか出力されない)
  • Service の 第2引数で利用する metadata 対応が nestjs オプションを有効化したときにしかきかない

今回の調査をきっかけに、今後 gRPC 関連の処理を見直す際に検討したいと思える、良いライブラリと出会えてよかったです。

まとめ

今回の調査・検証結果をまとめると以下のとおりです。

  • protobuf v3.12 から optional が実験的に復活し、内部的に oneof として扱われている
  • protobuf のコード生成ライブラリ
    • Rust で field presence の対応が入った crate は現時点では見つけられず
    • JavaScript で field presence を使う場合は公式の protoc の js_out で対応されている
    • TypeScript で field presence を使う場合は ts-proto で対応されている
      • protoc の TS 向けプラグインには対応されているものはなかった

参考

以下は調査をする中で見つけたサイトの紹介です。

NestJSでgRPC API作るならコード生成はts-protoで決まり

ライブラリの調査しているときに見つけた、ts-proto + NestJS で gRPC サーバーを作る記事です。 ありがたいことに以前のgRPCの記事へリンクしていただけてました。感謝です。 https://qiita.com/vol1003/items/326a074fb1a605651750

protobuf の optional な値の扱い方とField Presence について

チームで検討していたことが簡潔にまとまっており、proto3 の optional についても詳しく書かれていました。 細かく仕様を見てみようと思った矢先に見つけたので、今回の記事では大幅に端折りました。いいまとめをありがとうございます。 https://note.com/dd_techblog/n/n95e4331a8eea

おわりに

日頃の開発をするなかで、protobuf での optional な値の扱い方をどうするかを定期的に話し合ってきていましたが、調査・検討をするタイミングを作ることができずにいたため、いい機会になりました。 また、この記事が optional な値の扱い方に困っている方へ多少の手助けになると幸いです。


CADDiでは「モノづくり産業のポテンシャルを解放する」ための仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。

少しでも興味を持っていただけましたたら、リニューアルされたばかりの 採用サイト をご覧ください。(わかりやすく、ヘッダの仕掛けもかっこいいので是非!)

また、カジュアル面談も行っていますので、実際にエンジニアに会ってみたい方はこちらからどうぞ。