Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DeNA TechCon 2021 - スマホ向けゲームの辛い部分をコード自動生成技術で克服する / Overcoming the Painful Part of Smartphone Games Development with Automatic Code Generation

DeNA TechCon 2021 - スマホ向けゲームの辛い部分をコード自動生成技術で克服する / Overcoming the Painful Part of Smartphone Games Development with Automatic Code Generation

2021/03/03 に開催されるDeNA TechCon 2021の発表資料です。

スマホ向けゲーム開発で書くことが多いブリッジコードを、Protocol Buffers Compiler Pluginを使って自動生成するという内容です。詳しくは公式ページのセッション概要をご参照ください。

公開日時: 2021/03/03 13:00@Track C

TOYAMA Sumio

March 03, 2021
Tweet

More Decks by TOYAMA Sumio

Other Decks in Programming

Transcript

  1. 2 自己紹介 p 氏名: 外山 純生 (TOYAMA Sumio) @sumio_tym (Twitter)

    / @sumio (GitHub) p 所属: DeNA SWETグループ(Software Engineer in Test) / ソリューション事業本部(兼務) p 業務内容: 品質のボトルネック解決 (主にAndroid) p その他: 「Androidテスト全書」執筆 https://peaks.cc/sumio_tym/android_testing
  2. 4 話の流れ 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol

    Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  3. 5 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  4. 6 Unityについて p ゲームエンジンのひとつ https://unity.com/ p クロスプラットフォーム p C#で書けば、原則Android/iOS両対応のゲームが作れる p

    上記で書けないAndroid/iOSの機能を使いたいときは p Android/iOSのネイティブライブラリ(後述)を呼び出せる p ネイティブライブラリ呼び出しにはブリッジコード(後述)が必要
  5. 9 ブリッジコード p 異なるプログラミング言語間の呼び出しコード p ネイティブライブラリを呼ぶために必要 Unity (C#) fun doSomething(

    param: MyData, callback: ((MyResult) -> Unit) ) Android (Kotlin) doSomething(myData, myCallback) myCallback(myResult) ブリッジコード (言語の境界を越えるために特別なルールに従う必要)
  6. 10 Android→Unity呼び出し時のルール p 呼び出し先(callee)を指定するとき p クラス名やメソッド名は文字列で指定 p 引数を指定するとき p 引数の数は1つだけ

    p 引数の型は文字列だけ ルールの内容は 以下の組み合わせによって それぞれ異なります • ゲームエンジンの種類 • Android or iOS • 呼び出す方向
  7. 11 Android→Unity呼び出しのコード例 val unityPlayer = Class.forName("com.unity3d.player.UnityPlayer") val sendMessage = unityPlayer.getMethod("UnitySendMessage",

    String::class.java, String::class.java, String::class.java) sendMessage.invoke(null, "MyGame", "Foo", "MyParam") MyGameのFooメソッドの引数に"MyParam"を指定 して呼び出す場合 文字列のメソッド名 引数1つ。文字列型のみ
  8. Android層 Unity層 13 ルールを踏まえたブリッジ処理の流れ UnityからdoSomething(myData, myCallback)を呼ぶ例 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録

    ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ... フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) }
  9. Android層 Unity層 14 ルールを踏まえたブリッジ処理の流れ UnityからdoSomething(myData, myCallback)を呼ぶ例 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録

    ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ... フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } ⑦本来のdoSomething()が完了。 処理結果myResultを引数にコールバックが呼ばれる
  10. Android層 Unity層 15 ルールを踏まえたブリッジ処理の流れ UnityからdoSomething(myData, myCallback)を呼ぶ例 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録

    ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ... フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } ⑦本来のdoSomething()が完了。 処理結果myResultを引数にコールバックが呼ばれる フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ
  11. Android層 Unity層 フェーズ4: HandleCallback(⑧)の処理 ⑩⑧をデシリアライズしてIDとmyResultを復元 ⑪IDからテーブルを引いてmyCallbackを取り出す ⑫myCallback(myResult)を呼ぶ 16 ルールを踏まえたブリッジ処理の流れ UnityからdoSomething(myData,

    myCallback)を呼ぶ例 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録 ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ... フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } ⑦本来のdoSomething()が完了。 処理結果myResultを引数にコールバックが呼ばれる フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ
  12. 18 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  13. 22 コード生成ツールの要件 p 既存の(自動生成しない部分の)コードへの影響が少ないこと p typoや型不一致をビルド時に発見できること p ブリッジコードを生成するとき p 生成されたコードをコンパイルするとき

    p 複数言語で共通に使われるものを1箇所でマスター管理 できること p (デ)シリアライズ対象のデータ構造 (スキーマ定義) p メソッドシグネチャー (名前と引数の並び)
  14. 24 ここまでのまとめ p 手書きしたブリッジコードのミスをコンパイラが 発見するのは難しい p typoやJSONの型不一致など p ブリッジコード起因のバグも工数も1/6を占めていた p

    ブリッジコードを自動生成することで解決したい p ビルド時にミスを発見し、二重管理を排除したい p Protocol Buffersを採用
  15. 25 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  16. 26 Protocol Buffersの特徴 https://developers.google.com/protocol-buffers p データのシリアライズ形式の1つ p バイナリフォーマット p データ構造(スキーマ)はprotoファイルに定義

    p protoファイルから以下を各言語ごとに自動生成 (Protocol Buffers Compiler: protocコマンド) p protoスキーマに対応するクラス定義 型安全! typoや型不一致はコンパイルエラー
  17. 27 Protocol Buffersの利用例 https://developers.google.com/protocol-buffers より message Person { optional string

    name = 1; optional int32 id = 2; optional string email = 3; } person.proto (スキーマ定義) Person john = Person.newBuilder() .setId(1234) .setName("John Doe") .setEmail("[email protected]") .build(); FileOutputStream output = new FileOutputStream(args[0]); john.writeTo(output); Javaでシリアライズする例
  18. 28 protocコマンド message Person { optional string name = 1;

    optional int32 id = 2; optional string email = 3; } person.proto (スキーマ定義) public final class PersonOuterClass { ... public static final class Person ... { ... public String getName() { ... } ... } } ./gen/PersonOuterClass.java protoc --java_out=lite:./gen person.proto ※Androidではliteオプションが必要 ※
  19. 29 protocプラグイン person.proto ./gen/PersonOuterClass.java OUT_DIR/{プラグインが生成したファイル} protoc --plugin=protoc-gen-NAME=path/to/myplugin --NAME_out=opt1=value1,opt2=value2,...:OUT_DIR --java_out=lite:./gen person.proto

    NAME: プラグインの名前 path/to/myplugin: プラグイン実行ファイル (CLI) opt1=value1: プラグインに渡すオプション OUT_DIR: プラグインが生成するコードの出力先 検索キーワード: google.protobuf.compiler.plugin.h
  20. 30 protocプラグインのインターフェイス p CLIプログラム p 標準入力: protoファイルに書かれている情報 p message内のフィールドの型や名前など p

    protoファイルに自由に設定できるカスタムオプションの値 p 標準出力: 生成したいファイルの情報 p 標準入出力はprotobuf形式のバイナリデータ p google/protobuf/compiler/plugin.proto (GitHub.com) p google/protobuf/descriptor.proto (GitHub.com) protoファイルをマスターデータにできそう
  21. 31 ふたたびコード生成ツールの要件 p 既存の(自動生成しない部分の)コードへの影響が少ないこと → (後述)protocが生成したコードはブリッジ内でしか使わない p typoや型不一致をビルド時に発見できること → protoc生成コードとの整合性は、その言語のコンパイラが保証!

    → protocプラグインで自動生成時のチェックも可能 p 複数言語で共通に使われるものを1箇所でマスター管理 できること → マスター管理対象が増えても、カスタムオプションとして定義 すればprotoファイル1つで管理できる!
  22. 33 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  23. 34 自動生成の対象(コード生成ツールの出力) 現バージョンではAndroid層が対象 Android層 Unity層 フェーズ4: HandleCallback(⑧)の処理 ⑩⑧をデシリアライズしてIDとmyResultを復元 ⑪IDからテーブルを引いてmyCallbackを取り出す ⑫myCallback(myResult)を呼ぶ

    34 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録 ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ... フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } ⑦本来のdoSomething()が完了。 処理結果myResultを引数にコールバックが呼ばれる フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ この部分を自動生成する
  24. Android層 35 ⑤の部分をもう少し詳しく フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result

    -> ⑧以降の処理(ID, result) } フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ val wireBinary = ③から取り出したmyData部分のprotobufバイナリ val proto: PbMyData = ProtoMyData.parseFrom(wireBinary) val myData = toOurObject(proto) protocが生成したクラス 既存クラスにデータを詰め替える関数。本ツールで生成。
  25. Android層 36 ⑧の部分をもう少し詳しく フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result

    -> ⑧以降の処理(ID, result) } フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ val proto : PbMyResult = toProtoObject(myResult) val base64 = Base64.encodeToString(proto.toByteArray(), NO_WRAP) val ⑧ = (IDとbinBase64をJSON配列に詰めたもの) protocが生成したクラス protocが生成したクラスへデータを詰め替える関数。本ツールで生成。
  26. 37 生成するものまとめ p ブリッジコード本体 p データを詰め替える関数 p 前掲のtoOurObject()・toProtoObject() p クラスごとに1組ずつ必要

    フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ
  27. 38 コード生成に必要な情報① p Unity側に公開したい関数の情報 fun doSomething2(MyData1, MyData2, (MyResult?, MyError?) ->

    Unit)) p 関数の名前 p doSomething2 p 引数の並びと型(コールバックが受け取る引数も) p MyData1, MyData2, MyResult?, MyError?
  28. 41 コード生成に必要な情報③ p (デ)シリアライズが必要な型に対応するprotoスキーマ定義 MyData2 MyResult MyError Foo Bar Baz

    doSomething2(MyData1, MyData2, (MyResult?, MyError?) -> Unit) MyData1 message Foo { ... } message Bar { ... } message MyData2 { ... } message MyResult { ... } message MyResult { ... } message MyData1 { ... } message Baz { ... }
  29. 43 コード生成に必要な情報まとめ 必要な情報 情報源 ① Unity側に公開する 関数の情報 ネイティブライブラリ ② (デ)シリアライズが

    必要な型のクラス定義 ネイティブライブラリ ③ ②に対応するproto スキーマ定義 protoスキーマ定義 ファイル
  30. 44 コード生成に必要な情報まとめ 必要な情報 情報源 アクセス手段 ① Unity側に公開する 関数の情報 ネイティブライブラリ Kotlin

    リフレクションAPI ② (デ)シリアライズが 必要な型のクラス定義 ネイティブライブラリ Kotlin リフレクションAPI ③ ②に対応するproto スキーマ定義 protoスキーマ定義 ファイル protocプラグインの 標準入力
  31. 45 データの流れ ネイティブライブラリ.jar protoスキーマ定義 ファイル protocコマンド 本コード生成ツール (protocプラグイン) proto定義ファイルの情報 (code

    generator request) 生成するファイルの情報 (code generator response) ダイナミックロード リフレクションAPIで情報取得 • Unity側に公開する関数(※)リスト • そこから使われる引数の情報 protocによる生成コード 本ツールによる生成コード 凡例 入力 既存ツール 開発対象 生成物 (入力) (入力) (既存ツール) (生成物) (生成物) (開発対象) ※Unity側に公開する関数には独自アノテーションを付けて区別可能にしています
  32. 46 コード生成ツール導入前後の比較 Before After 手書き するもの ブリッジコード • (デ)シリアライズが必要なクラスに対応する protoスキーマ定義

    • Unity側に公開する関数へのアノテーション付与 • (詳細は割愛)enum対応のための書き換え 自動生成 なし ブリッジコード Android iOS Unity Cocos2d-x 今回の範囲
  33. 47 ここまでのまとめ 開発したコード生成ツールの概要を説明しました p 入力: ネイティブライブラリとprotoスキーマ定義 p [アノテーション付与が必要] Unity側に公開する関数の情報 p

    [自動取得] (デ)シリアライズが必要なクラスの定義 p [手書きが必要] そのprotoスキーマ定義 p 出力: ブリッジコード p ブリッジコード本体 p データを詰め替える関数
  34. 48 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  35. 49 protoスキーマ定義 ネイティブライブラリ.jar protoスキーマ定義 ファイル protocコマンド 本コード生成ツール (protocプラグイン) proto定義ファイルの情報 (code

    generator request) 生成するファイルの情報 (code generator response) ダイナミックロード リフレクションAPIで情報取得 • Unity側に公開する関数(※)リスト • そこから使われる引数の情報 protocによる生成コード 本ツールによる生成コード 凡例 入力 既存ツール 開発対象 生成物 (入力) (入力) (既存ツール) (生成物) (生成物) (開発対象) ※Unity側に公開する関数には独自アノテーションを付けて区別可能にしています この部分
  36. 52 ネイティブライブラリ側のクラスと対応付ける② protoファイルにカスタムオプションを定義して実現 package example data class Person( val name:

    String, val tel: String? ) message PbPerson { option (example.message_options) = { java_name = "example.Person" }; optional string name = 1; ... } 対応するクラスのFQCNを 宣言してもらう ネイティブライブラリ クラス定義 protoスキーマ定義
  37. 55 nullabilityを扱えるようにする③ package example data class Person(val name: String, val

    tel: String?) message PbPerson { ... optional string tel = 2 [(example.field_options) = { nullable: true }]; } nullabilityを宣言してもらう ネイティブライブラリ クラス定義 protoスキーマ定義 protoファイルにカスタムオプションを定義して実現
  38. 56 nullabilityを扱えるようにする④ package example data class Person(val name: String, val

    tel: String?) // protoc生成クラスから詰め替える fun toOurObject(protoObject: PbPerson) : Person = Person(name = pbPerson.name, tel = if pbPerson.hasTel() pbPerson.tel else null) 生成コードにおけるnull/non-nullの違い
  39. 57 特別な型に対応する① p protobufでシリアライズできるのはmessageだけ doSomething3(MyData1, List<MyData2>, (MyResult?, MyError?) -> Unit)

    messageのリストなのでシリアライズ不可 message PbMyData2List { option (example.message_options) = { container_type: LIST }; repeated PbMyData2 my_data2 = 1; } ➜ ラップするmessageを明示的に定義する ラップしていることを カスタムオプションで宣言
  40. 58 特別な型に対応する② p protobufでnullがシリアライズできない doSomething3(MyData1, List<MyData2>, (MyResult?, MyError?) -> Unit)

    これらの引数にnullが渡されるとシリアライズできない ➜ 複数の引数をJSON配列にまとめる時にJSON nullで表現 val json = JSONArray() val params = (引数リストに対応するprotoc生成のオブジェクト) params.forEach { if (it == null) json.put(JSONObject.NULL) else json.put(Base64.encodeToString(it.toByteArray(), ...) } sendMessage.invoke(null, "MyGame", "HandleCallback", json.toString())
  41. 60 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  42. 61 KotlinリフレクションAPIを使ったコード解析 ネイティブライブラリ.jar protoスキーマ定義 ファイル protocコマンド 本コード生成ツール (protocプラグイン) proto定義ファイルの情報 (code

    generator request) 生成するファイルの情報 (code generator response) ダイナミックロード リフレクションAPIで情報取得 • Unity側に公開する関数(※)リスト • そこから使われる引数の情報 protocによる生成コード 本ツールによる生成コード 凡例 入力 既存ツール 開発対象 生成物 (入力) (入力) (既存ツール) (生成物) (生成物) (開発対象) ※Unity側に公開する関数には独自アノテーションを付けて区別可能にしています この部分
  43. 64 Javaのサポート可否① p 解析対象がJavaでも困ることは少ないが、 nullability判定が困難 p KTypeクラスのisMarkedNullableプロパティ p Javaのnullability annotationをサポートしているが、

    AndroidXのアノテーションは対象外 p Java由来クラスかどうか判定するAPIも無い p kotlin.Metadataアノテーションで判定可能との情報あり https://stackoverflow.com/a/39806722/2925059
  44. 68 (デ)シリアライズ対象の型の範囲① p protoスキーマ定義で表現不可なもの p ListのList p nullableなList p Generics

    p 要素がnullableなList p Map (ハッシュテーブル) p 特別なmessageを用意すれば対応できるが、複雑度が増す ➜ どうしても必要になるまでサポートしない
  45. 69 (デ)シリアライズ対象の型の範囲② p (Unity側に公開する関数の)コールバック引数 p λ式 p SAMインターフェイス p 複数メソッドを持つインターフェイス

    p 範囲を広げるとコールバックに渡される引数の特定が困難に ➜ できるかぎり1種類に限定する ➜ Kotlinで良く使われるλ式のみに絞った (Function<out R>のサブタイプ)
  46. 71 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか

    2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
  47. 74 テスト観点ごとに発見すべき箇所を検討 ①ツール による チェック ②コ ンパ イラ ③生成 コードへの

    テスト ④Unity 側との 結合 ⑤ 手動 protoスキーマ定義忘れ ◦ フィールド過不足 ◦ [データ詰め替え関数] プロパティが過不足なくコピーされているか ◦ 異なるプラットフォームで シリアライズ結果が一致しているか ◦ ネイティブの処理結果が コールバックを通じて正しく返ってくるか ◦ ・・・ 漏れているテストはこの時点で発見・追加
  48. 77 全体のまとめ p 制約の厳しいブリッジコードを手で書くのが辛いので、 自動生成するツールを開発しました (現時点で完成しているAndroid側のみ) p protocプラグインとKotlinリフレクションを使うことで 整合性のチェックも可能になった p

    コード自動生成ツール開発のポイントを紹介しました p protoカスタムオプションで必要な情報を宣言する p ネイティブ側の言語機能の対応は必要最低限にする p テスト観点ごとにバグ発見箇所を検討したら満足する品質になった
  49. 78 参考URL p Unity2019.4ユーザーズマニュアル「JARプラグイン」 https://docs.unity3d.com/ja/2019.4/Manual/AndroidJARPlugins.html p Protocol Buffers公式ドキュメント https://developers.google.com/protocol-buffers p

    protocプラグインのコマンドラインオプションの仕様 https://developers.google.com/protocol- buffers/docs/reference/cpp/google.protobuf.compiler.plugin p 「protocプラグインとカスタムオプション」by @yugui https://qiita.com/yugui/items/29adefab34f7f1a3c3c6 p 「protocプラグインの書き方」 by @yugui https://qiita.com/yugui/items/87d00d77dee159e74886