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
4 話の流れ 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
5 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
6 Unityについて p ゲームエンジンのひとつ https://unity.com/ p クロスプラットフォーム p C#で書けば、原則Android/iOS両対応のゲームが作れる p 上記で書けないAndroid/iOSの機能を使いたいときは p Android/iOSのネイティブライブラリ(後述)を呼び出せる p ネイティブライブラリ呼び出しにはブリッジコード(後述)が必要
10 Android→Unity呼び出し時のルール p 呼び出し先(callee)を指定するとき p クラス名やメソッド名は文字列で指定 p 引数を指定するとき p 引数の数は1つだけ p 引数の型は文字列だけ ルールの内容は 以下の組み合わせによって それぞれ異なります • ゲームエンジンの種類 • Android or iOS • 呼び出す方向
18 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
20 従前のブリッジコードが抱えていた課題 p 文字列のtypoがコンパイル時に発見できない p 異常時にしか使われないJSONフィールドのtypoだと ずっと見付からないことも・・・ p JSONを介するやりとりが型安全ではない p ブリッジコードが原因のバグの割合: 約1/6 p ブリッジコードの実装工数の割合: 約1/6
22 コード生成ツールの要件 p 既存の(自動生成しない部分の)コードへの影響が少ないこと p typoや型不一致をビルド時に発見できること p ブリッジコードを生成するとき p 生成されたコードをコンパイルするとき p 複数言語で共通に使われるものを1箇所でマスター管理 できること p (デ)シリアライズ対象のデータ構造 (スキーマ定義) p メソッドシグネチャー (名前と引数の並び)
24 ここまでのまとめ p 手書きしたブリッジコードのミスをコンパイラが 発見するのは難しい p typoやJSONの型不一致など p ブリッジコード起因のバグも工数も1/6を占めていた p ブリッジコードを自動生成することで解決したい p ビルド時にミスを発見し、二重管理を排除したい p Protocol Buffersを採用
25 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
26 Protocol Buffersの特徴 https://developers.google.com/protocol-buffers p データのシリアライズ形式の1つ p バイナリフォーマット p データ構造(スキーマ)はprotoファイルに定義 p protoファイルから以下を各言語ごとに自動生成 (Protocol Buffers Compiler: protocコマンド) p protoスキーマに対応するクラス定義 型安全! typoや型不一致はコンパイルエラー
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オプションが必要 ※
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ファイルをマスターデータにできそう
32 ここまでのまとめ Protocol Buffersの特徴と採用理由を説明しました p データ構造に対応するクラスが言語ごとに生成されるので コンパイラによるチェックが効く p 自動生成時にprotoファイルの情報にアクセスできる仕組み (protocプラグイン)が用意されている p カスタムオプションによるprotoファイルの拡張性
33 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
37 生成するものまとめ p ブリッジコード本体 p データを詰め替える関数 p 前掲のtoOurObject()・toProtoObject() p クラスごとに1組ずつ必要 フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ
38 コード生成に必要な情報① p Unity側に公開したい関数の情報 fun doSomething2(MyData1, MyData2, (MyResult?, MyError?) -> Unit)) p 関数の名前 p doSomething2 p 引数の並びと型(コールバックが受け取る引数も) p MyData1, MyData2, MyResult?, MyError?
47 ここまでのまとめ 開発したコード生成ツールの概要を説明しました p 入力: ネイティブライブラリとprotoスキーマ定義 p [アノテーション付与が必要] Unity側に公開する関数の情報 p [自動取得] (デ)シリアライズが必要なクラスの定義 p [手書きが必要] そのprotoスキーマ定義 p 出力: ブリッジコード p ブリッジコード本体 p データを詰め替える関数
48 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
51 ネイティブライブラリ側のクラスと対応付ける① 対応付けによる効果 p データ詰め替え関数の引数・戻り値の型が確定する fun toProtoObject(ours: Person) : PbPerson p 両者の矛盾を検知できる p フィールド名不一致 p 型不一致 p nullability不一致
53 nullabilityを扱えるようにする① p protobufの世界では原則nullable p non-nullを意味するrequiredキーワードは非推奨 p 値が未設定=nullという考え方 p 未設定のときはデフォルト値が返るので、 未設定のときの意図がどちらか判別できない p デフォルト値をセットしたつもり p nullをセットしたつもり
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の違い
59 ここまでのまとめ p protobufの世界と対象言語(ネイティブライブラリ)の世界 を対応付けるためにカスタムオプションを活用しよう p 対応クラス名、nullability宣言、etc. p コード生成時に整合性チェックできるようになる p protobufで非対応の型は1つ1つ対応を検討する p messageのリスト、null、etc.
60 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
62 Kotlinリフレクションの用途 p コード生成に必要な情報のうち、 ネイティブライブラリにしか無い情報を取得する p Unity側に公開する関数の情報 p (デ)シリアライズが必要な型のクラス定義 p protoスキーマ定義との整合性をチェックする p nullability不一致の検知 p etc.
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
65 Javaのサポート可否② p Javaはサポート対象外にした p 正確には以下のコストを比較して判断 p 解析対象クラスをKotlinにコンバートするコスト (Android Studioの機能で簡単にコンバート可能) p コード解析時にJava由来クラスだけ特別扱いするコスト p nullability以外にもgetterとpropertyの違いなど
66 (デ)シリアライズ対象クラス定義の自由度① protoc生成クラスからデータを詰め替えるとき p どうやって初期化するか曖昧 p コンストラクタ引数で初期化 p 空のオブジェクト生成→各プロパティにセットして初期化 fun toOurObject(protoObject: PbPerson) = Person(name = pbPerson.name, tel = ...) fun toOurObject(protoObject: PbPerson) = Person().also { it.name = pbPerson.name ... }
67 (デ)シリアライズ対象クラス定義の自由度② p Kotlin data classに限定 p プロパティをコンストラクタで初期化する書き方が多い p コンストラクタ引数でのプロパティ初期化に限定 data class MyData(val prop1: String, val prop2: String?) data class MyData(val prop1: String) { var prop2: String? = null }
68 (デ)シリアライズ対象の型の範囲① p protoスキーマ定義で表現不可なもの p ListのList p nullableなList p Generics p 要素がnullableなList p Map (ハッシュテーブル) p 特別なmessageを用意すれば対応できるが、複雑度が増す ➜ どうしても必要になるまでサポートしない
69 (デ)シリアライズ対象の型の範囲② p (Unity側に公開する関数の)コールバック引数 p λ式 p SAMインターフェイス p 複数メソッドを持つインターフェイス p 範囲を広げるとコールバックに渡される引数の特定が困難に ➜ できるかぎり1種類に限定する ➜ Kotlinで良く使われるλ式のみに絞った (Functionのサブタイプ)
70 ここまでのまとめ ネイティブライブラリ側の自由度をできるだけ狭める p 自由度が高まると、解析ツールの実装が複雑に・・ p Javaはサポート対象外にする p 対応するprotoスキーマ定義が無いときに無理に対応しない p ListのListなど p コールバック引数として指定できる型はλ式に限定する
71 1. 背景 p ネイティブライブラリとブリッジコード p コード自動生成ツール開発のきっかけ p 何故Protocol Buffersにしたのか 2. 開発したコード自動生成ツールの概要 3. コード自動生成する上でのポイント p protoスキーマ定義 p KotlinリフレクションAPIを使ったコード解析 4. コード自動生成ツールにバグを入れ込まない工夫
73 生成コードにバグを入れないためには 次のどこか(できる限り④まで)で阻止(発見)できれば良い ① コード生成ツールによる整合性チェック p コード生成ツールへのユニットテストでチェックの正当性担保 ② ツールが生成したコードのコンパイル時チェック ③ ツールが生成したコードに対するユニットテスト ④ ゲーム(Unity)側との結合テスト(自動テスト) ⑤ 手動の結合テスト・E2Eテスト
77 全体のまとめ p 制約の厳しいブリッジコードを手で書くのが辛いので、 自動生成するツールを開発しました (現時点で完成しているAndroid側のみ) p protocプラグインとKotlinリフレクションを使うことで 整合性のチェックも可能になった p コード自動生成ツール開発のポイントを紹介しました p protoカスタムオプションで必要な情報を宣言する p ネイティブ側の言語機能の対応は必要最低限にする p テスト観点ごとにバグ発見箇所を検討したら満足する品質になった
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