Slide 1

Slide 1 text

スマホ向けゲームの辛い部分を コード自動生成技術で克服する 外山 純生 (sumio_tym)

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 お話しすること スマホゲームのブリッジコード自動生成ツール開発 で得られた知見を紹介します (現時点で完成しているAndroid側のみ) p コード自動生成ツール開発のきっかけと概要 p コード自動生成する上でのポイント 異なるプログラミング言語間の 呼び出しを実現するコード (後で詳しく説明します) Unity

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

6 Unityについて p ゲームエンジンのひとつ https://unity.com/ p クロスプラットフォーム p C#で書けば、原則Android/iOS両対応のゲームが作れる p 上記で書けないAndroid/iOSの機能を使いたいときは p Android/iOSのネイティブライブラリ(後述)を呼び出せる p ネイティブライブラリ呼び出しにはブリッジコード(後述)が必要

Slide 7

Slide 7 text

7 スマホゲーム文脈でのネイティブライブラリ p AndroidやiOSのネイティブで書かれたライブラリ p Androidのネイティブ: Kotlin/Java p iOSのネイティブ: Swift/Objective-C p Unity用語: ネイティブプラグイン

Slide 8

Slide 8 text

8 ネイティブライブラリのユースケース p ネイティブならではの機能を使いたい p 課金 p ネイティブ向けのみ提供のライブラリを使いたい p サードパーティ製のライブラリ p 複数のゲームエンジンから利用したい p Unity・Cocos2d-x両方から使いたい共通ロジック

Slide 9

Slide 9 text

9 ブリッジコード p 異なるプログラミング言語間の呼び出しコード p ネイティブライブラリを呼ぶために必要 Unity (C#) fun doSomething( param: MyData, callback: ((MyResult) -> Unit) ) Android (Kotlin) doSomething(myData, myCallback) myCallback(myResult) ブリッジコード (言語の境界を越えるために特別なルールに従う必要)

Slide 10

Slide 10 text

10 Android→Unity呼び出し時のルール p 呼び出し先(callee)を指定するとき p クラス名やメソッド名は文字列で指定 p 引数を指定するとき p 引数の数は1つだけ p 引数の型は文字列だけ ルールの内容は 以下の組み合わせによって それぞれ異なります • ゲームエンジンの種類 • Android or iOS • 呼び出す方向

Slide 11

Slide 11 text

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つ。文字列型のみ

Slide 12

Slide 12 text

Android層 Unity層 12 ルールを踏まえたブリッジ処理の流れ UnityからdoSomething(myData, myCallback)を呼ぶ例 フェーズ1: 開始 ①コールバック登録用IDを払い出す ②myCallbackをテーブルに登録 ③myDataとIDを文字列にシリアライズ ④Android側の_doSomething(③)を呼ぶ ID コールバック 1 myCallback 2 ...

Slide 13

Slide 13 text

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) }

Slide 14

Slide 14 text

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を引数にコールバックが呼ばれる

Slide 15

Slide 15 text

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(⑧)を呼ぶ

Slide 16

Slide 16 text

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(⑧)を呼ぶ

Slide 17

Slide 17 text

17 ここまでのまとめ p ネイティブライブラリ呼び出しには ブリッジコードが必要 p ブリッジ処理は制約を踏まえた実装が必要 p 呼び出し先は文字列で指定 p 引数列の(デ)シリアライズが必要 p JSON p Protocol Buffers p etc.

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19 従前のブリッジコード 手書きで頑張っていた p 呼び出し先メソッド名の文字列 p 引数のシリアライズ・デシリアライズ p JSON (フィールド名文字列も手書き)! Android iOS Unity Cocos2d-x ① ② ③ ④ ⑤ ⑥ ⑦ ⑧

Slide 20

Slide 20 text

20 従前のブリッジコードが抱えていた課題 p 文字列のtypoがコンパイル時に発見できない p 異常時にしか使われないJSONフィールドのtypoだと ずっと見付からないことも・・・ p JSONを介するやりとりが型安全ではない p ブリッジコードが原因のバグの割合: 約1/6 p ブリッジコードの実装工数の割合: 約1/6

Slide 21

Slide 21 text

21 そこでコード自動生成! p typoは無くなるはず p コード生成時に型チェックも行えるはず p バグも工数も1/6減らせるはず

Slide 22

Slide 22 text

22 コード生成ツールの要件 p 既存の(自動生成しない部分の)コードへの影響が少ないこと p typoや型不一致をビルド時に発見できること p ブリッジコードを生成するとき p 生成されたコードをコンパイルするとき p 複数言語で共通に使われるものを1箇所でマスター管理 できること p (デ)シリアライズ対象のデータ構造 (スキーマ定義) p メソッドシグネチャー (名前と引数の並び)

Slide 23

Slide 23 text

23 採用した方式 (デ)シリアライズ形式 Protocol Buffers マスターデータ protoスキーマ定義ファイル コード生成方式 protocプラグイン (Protocol Buffers Compiler Plugin)

Slide 24

Slide 24 text

24 ここまでのまとめ p 手書きしたブリッジコードのミスをコンパイラが 発見するのは難しい p typoやJSONの型不一致など p ブリッジコード起因のバグも工数も1/6を占めていた p ブリッジコードを自動生成することで解決したい p ビルド時にミスを発見し、二重管理を排除したい p Protocol Buffersを採用

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

26 Protocol Buffersの特徴 https://developers.google.com/protocol-buffers p データのシリアライズ形式の1つ p バイナリフォーマット p データ構造(スキーマ)はprotoファイルに定義 p protoファイルから以下を各言語ごとに自動生成 (Protocol Buffers Compiler: protocコマンド) p protoスキーマに対応するクラス定義 型安全! typoや型不一致はコンパイルエラー

Slide 27

Slide 27 text

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でシリアライズする例

Slide 28

Slide 28 text

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オプションが必要 ※

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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ファイルをマスターデータにできそう

Slide 31

Slide 31 text

31 ふたたびコード生成ツールの要件 p 既存の(自動生成しない部分の)コードへの影響が少ないこと → (後述)protocが生成したコードはブリッジ内でしか使わない p typoや型不一致をビルド時に発見できること → protoc生成コードとの整合性は、その言語のコンパイラが保証! → protocプラグインで自動生成時のチェックも可能 p 複数言語で共通に使われるものを1箇所でマスター管理 できること → マスター管理対象が増えても、カスタムオプションとして定義 すればprotoファイル1つで管理できる!

Slide 32

Slide 32 text

32 ここまでのまとめ Protocol Buffersの特徴と採用理由を説明しました p データ構造に対応するクラスが言語ごとに生成されるので コンパイラによるチェックが効く p 自動生成時にprotoファイルの情報にアクセスできる仕組み (protocプラグイン)が用意されている p カスタムオプションによるprotoファイルの拡張性

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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(⑧)を呼ぶ この部分を自動生成する

Slide 35

Slide 35 text

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が生成したクラス 既存クラスにデータを詰め替える関数。本ツールで生成。

Slide 36

Slide 36 text

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が生成したクラスへデータを詰め替える関数。本ツールで生成。

Slide 37

Slide 37 text

37 生成するものまとめ p ブリッジコード本体 p データを詰め替える関数 p 前掲のtoOurObject()・toProtoObject() p クラスごとに1組ずつ必要 フェーズ2: _doSomething(③)の処理 ⑤③をデシリアライズしてIDとmyDataを復元 ⑥本来の処理を呼ぶ doSomething(myData) { result -> ⑧以降の処理(ID, result) } フェーズ3: コールバックの処理 ⑧処理結果myResultとIDを文字列にシリアライズ ⑨Unity側のHandleCallback(⑧)を呼ぶ

Slide 38

Slide 38 text

38 コード生成に必要な情報① p Unity側に公開したい関数の情報 fun doSomething2(MyData1, MyData2, (MyResult?, MyError?) -> Unit)) p 関数の名前 p doSomething2 p 引数の並びと型(コールバックが受け取る引数も) p MyData1, MyData2, MyResult?, MyError?

Slide 39

Slide 39 text

39 コード生成に必要な情報② p (デ)シリアライズが必要な型の情報 p 前頁で集めた引数の型のクラス定義 p そこから参照されているクラスの定義 MyData1 MyData2 MyResult MyError Foo Bar Baz doSomething2(MyData1, MyData2, (MyResult?, MyError?) -> Unit)

Slide 40

Slide 40 text

40 コード生成に必要な情報② p (デ)シリアライズが必要な型の情報 p 前頁で集めた引数の型のクラス定義 p そこから参照されているクラスの定義 MyData1 MyData2 MyResult MyError Foo Bar Baz doSomething2(MyData1, MyData2, (MyResult?, MyError?) -> Unit) 必要な 情報の範囲

Slide 41

Slide 41 text

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 { ... }

Slide 42

Slide 42 text

42 コード生成に必要な情報まとめ 必要な情報 ① Unity側に公開する 関数の情報 ② (デ)シリアライズが 必要な型のクラス定義 ③ ②に対応するproto スキーマ定義

Slide 43

Slide 43 text

43 コード生成に必要な情報まとめ 必要な情報 情報源 ① Unity側に公開する 関数の情報 ネイティブライブラリ ② (デ)シリアライズが 必要な型のクラス定義 ネイティブライブラリ ③ ②に対応するproto スキーマ定義 protoスキーマ定義 ファイル

Slide 44

Slide 44 text

44 コード生成に必要な情報まとめ 必要な情報 情報源 アクセス手段 ① Unity側に公開する 関数の情報 ネイティブライブラリ Kotlin リフレクションAPI ② (デ)シリアライズが 必要な型のクラス定義 ネイティブライブラリ Kotlin リフレクションAPI ③ ②に対応するproto スキーマ定義 protoスキーマ定義 ファイル protocプラグインの 標準入力

Slide 45

Slide 45 text

45 データの流れ ネイティブライブラリ.jar protoスキーマ定義 ファイル protocコマンド 本コード生成ツール (protocプラグイン) proto定義ファイルの情報 (code generator request) 生成するファイルの情報 (code generator response) ダイナミックロード リフレクションAPIで情報取得 • Unity側に公開する関数(※)リスト • そこから使われる引数の情報 protocによる生成コード 本ツールによる生成コード 凡例 入力 既存ツール 開発対象 生成物 (入力) (入力) (既存ツール) (生成物) (生成物) (開発対象) ※Unity側に公開する関数には独自アノテーションを付けて区別可能にしています

Slide 46

Slide 46 text

46 コード生成ツール導入前後の比較 Before After 手書き するもの ブリッジコード • (デ)シリアライズが必要なクラスに対応する protoスキーマ定義 • Unity側に公開する関数へのアノテーション付与 • (詳細は割愛)enum対応のための書き換え 自動生成 なし ブリッジコード Android iOS Unity Cocos2d-x 今回の範囲

Slide 47

Slide 47 text

47 ここまでのまとめ 開発したコード生成ツールの概要を説明しました p 入力: ネイティブライブラリとprotoスキーマ定義 p [アノテーション付与が必要] Unity側に公開する関数の情報 p [自動取得] (デ)シリアライズが必要なクラスの定義 p [手書きが必要] そのprotoスキーマ定義 p 出力: ブリッジコード p ブリッジコード本体 p データを詰め替える関数

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

50 protoスキーマ定義のポイント p ネイティブライブラリ側のクラスと対応付ける p nullabilityを扱えるようにする p 特別な型に対応する

Slide 51

Slide 51 text

51 ネイティブライブラリ側のクラスと対応付ける① 対応付けによる効果 p データ詰め替え関数の引数・戻り値の型が確定する fun toProtoObject(ours: Person) : PbPerson p 両者の矛盾を検知できる p フィールド名不一致 p 型不一致 p nullability不一致

Slide 52

Slide 52 text

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スキーマ定義

Slide 53

Slide 53 text

53 nullabilityを扱えるようにする① p protobufの世界では原則nullable p non-nullを意味するrequiredキーワードは非推奨 p 値が未設定=nullという考え方 p 未設定のときはデフォルト値が返るので、 未設定のときの意図がどちらか判別できない p デフォルト値をセットしたつもり p nullをセットしたつもり

Slide 54

Slide 54 text

54 nullabilityを扱えるようにする② p ネイティブライブラリの世界では null/non-null両方有り得る p 対応するprotoフィールドも、nullかnon-nullか区別 しておきたい p コード生成時にnullability不一致をエラーにしたい

Slide 55

Slide 55 text

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ファイルにカスタムオプションを定義して実現

Slide 56

Slide 56 text

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の違い

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

58 特別な型に対応する② p protobufでnullがシリアライズできない doSomething3(MyData1, List, (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())

Slide 59

Slide 59 text

59 ここまでのまとめ p protobufの世界と対象言語(ネイティブライブラリ)の世界 を対応付けるためにカスタムオプションを活用しよう p 対応クラス名、nullability宣言、etc. p コード生成時に整合性チェックできるようになる p protobufで非対応の型は1つ1つ対応を検討する p messageのリスト、null、etc.

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

61 KotlinリフレクションAPIを使ったコード解析 ネイティブライブラリ.jar protoスキーマ定義 ファイル protocコマンド 本コード生成ツール (protocプラグイン) proto定義ファイルの情報 (code generator request) 生成するファイルの情報 (code generator response) ダイナミックロード リフレクションAPIで情報取得 • Unity側に公開する関数(※)リスト • そこから使われる引数の情報 protocによる生成コード 本ツールによる生成コード 凡例 入力 既存ツール 開発対象 生成物 (入力) (入力) (既存ツール) (生成物) (生成物) (開発対象) ※Unity側に公開する関数には独自アノテーションを付けて区別可能にしています この部分

Slide 62

Slide 62 text

62 Kotlinリフレクションの用途 p コード生成に必要な情報のうち、 ネイティブライブラリにしか無い情報を取得する p Unity側に公開する関数の情報 p (デ)シリアライズが必要な型のクラス定義 p protoスキーマ定義との整合性をチェックする p nullability不一致の検知 p etc.

Slide 63

Slide 63 text

63 Kotlinリフクレションの沼 p 言語機能のサポート範囲を増やすほど複雑化・・ p Javaのサポート可否 p (デ)シリアライズ対象クラス定義の自由度 p (デ)シリアライズ対象の型の範囲 いかにスコープ(サポート範囲)を狭められるかが鍵

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

65 Javaのサポート可否② p Javaはサポート対象外にした p 正確には以下のコストを比較して判断 p 解析対象クラスをKotlinにコンバートするコスト (Android Studioの機能で簡単にコンバート可能) p コード解析時にJava由来クラスだけ特別扱いするコスト p nullability以外にもgetterとpropertyの違いなど

Slide 66

Slide 66 text

66 (デ)シリアライズ対象クラス定義の自由度① protoc生成クラスからデータを詰め替えるとき p どうやって初期化するか曖昧 p コンストラクタ引数で初期化 p 空のオブジェクト生成→各プロパティにセットして初期化 fun toOurObject(protoObject: PbPerson) = Person(name = pbPerson.name, tel = ...) fun toOurObject(protoObject: PbPerson) = Person().also { it.name = pbPerson.name ... }

Slide 67

Slide 67 text

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 }

Slide 68

Slide 68 text

68 (デ)シリアライズ対象の型の範囲① p protoスキーマ定義で表現不可なもの p ListのList p nullableなList p Generics p 要素がnullableなList p Map (ハッシュテーブル) p 特別なmessageを用意すれば対応できるが、複雑度が増す ➜ どうしても必要になるまでサポートしない

Slide 69

Slide 69 text

69 (デ)シリアライズ対象の型の範囲② p (Unity側に公開する関数の)コールバック引数 p λ式 p SAMインターフェイス p 複数メソッドを持つインターフェイス p 範囲を広げるとコールバックに渡される引数の特定が困難に ➜ できるかぎり1種類に限定する ➜ Kotlinで良く使われるλ式のみに絞った (Functionのサブタイプ)

Slide 70

Slide 70 text

70 ここまでのまとめ ネイティブライブラリ側の自由度をできるだけ狭める p 自由度が高まると、解析ツールの実装が複雑に・・ p Javaはサポート対象外にする p 対応するprotoスキーマ定義が無いときに無理に対応しない p ListのListなど p コールバック引数として指定できる型はλ式に限定する

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

72 コード生成ツールに期待すること コンパイラ並みに高い期待をしてしまう・・・! p ツールが生成したコードにバグは無い p ツール導入によってバグが減るための大前提 p protoスキーマ定義の書き間違いなど、 ヒューマンエラーを検知してくれる

Slide 73

Slide 73 text

73 生成コードにバグを入れないためには 次のどこか(できる限り④まで)で阻止(発見)できれば良い ① コード生成ツールによる整合性チェック p コード生成ツールへのユニットテストでチェックの正当性担保 ② ツールが生成したコードのコンパイル時チェック ③ ツールが生成したコードに対するユニットテスト ④ ゲーム(Unity)側との結合テスト(自動テスト) ⑤ 手動の結合テスト・E2Eテスト

Slide 74

Slide 74 text

74 テスト観点ごとに発見すべき箇所を検討 ①ツール による チェック ②コ ンパ イラ ③生成 コードへの テスト ④Unity 側との 結合 ⑤ 手動 protoスキーマ定義忘れ ○ フィールド過不足 ○ [データ詰め替え関数] プロパティが過不足なくコピーされているか ○ 異なるプラットフォームで シリアライズ結果が一致しているか ○ ネイティブの処理結果が コールバックを通じて正しく返ってくるか ○ ・・・ 漏れているテストはこの時点で発見・追加

Slide 75

Slide 75 text

75 2つの公開関数に適用した結果 p QA段階ではバグ発見されず (それより前のフェーズで発見し尽くせた) p 生成コードへのテストは定型的: 自動生成できそう p 今回は手で書いた

Slide 76

Slide 76 text

76 ここまでのまとめ p コード生成ツールに期待される品質レベルは高い p 「どの種類のバグをどの段階のテストで発見すべ きか」のマトリックスを作成することで達成 p 漏れていたテストはこの時点で追加 p 将来的には生成コードに対するテストコードも 自動生成できそう

Slide 77

Slide 77 text

77 全体のまとめ p 制約の厳しいブリッジコードを手で書くのが辛いので、 自動生成するツールを開発しました (現時点で完成しているAndroid側のみ) p protocプラグインとKotlinリフレクションを使うことで 整合性のチェックも可能になった p コード自動生成ツール開発のポイントを紹介しました p protoカスタムオプションで必要な情報を宣言する p ネイティブ側の言語機能の対応は必要最低限にする p テスト観点ごとにバグ発見箇所を検討したら満足する品質になった

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

79 本セッションで紹介したコード自動生成技術が、 今後の皆さんの課題解決の選択肢のひとつに加わる と嬉しいです ありがとうございました!

Slide 80

Slide 80 text

No content