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. スマホ向けゲームの辛い部分を
    コード自動生成技術で克服する
    外山 純生 (sumio_tym)

    View Slide

  2. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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








    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. 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を明示的に定義する
    ラップしていることを
    カスタムオプションで宣言

    View Slide

  58. 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())

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  64. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  74. 74
    テスト観点ごとに発見すべき箇所を検討
    ①ツール
    による
    チェック
    ②コ
    ンパ
    イラ
    ③生成
    コードへの
    テスト
    ④Unity
    側との
    結合

    手動
    protoスキーマ定義忘れ ○
    フィールド過不足 ○
    [データ詰め替え関数]
    プロパティが過不足なくコピーされているか

    異なるプラットフォームで
    シリアライズ結果が一致しているか

    ネイティブの処理結果が
    コールバックを通じて正しく返ってくるか

    ・・・ 漏れているテストはこの時点で発見・追加

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  78. 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

    View Slide

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

    View Slide

  80. View Slide