Unity(IL2CPP環境)でのprotobufの使い方


概要

今年4月のprotobuf側の改修以降、IL2CPPのAOT対策が組み込まれたため、お世話になることが多いので解説を書く。



Protobuf is 何

ProtocolBufferというデータ形式。

データ -> protobuf形式(byte[]) -> データ というような変形ができる。


公式

https://developers.google.com/protocol-buffers/


特徴は、

・どんなデータを扱いたいかを定義した.protoという形式のファイルを書く

・エディタライブラリが定義ファイルからえいやっといろいろな言語向けのSerialize/Deserialize用コードを生成する(いろんな言語の実装が生成できる

・開発者は生成されたコードを叩くようなコードを書き、いろんな言語のインスタンス -> protobuf形式のデータ -> インスタンス とかの変形ができるようになる


syntax = "proto3";


message SearchRequest {

string query = 1;

int32 page_number = 2;

int32 result_per_page = 3;

}


protoファイルをサーバとクライアントで共有しとけばあとはなんとかなる的な。



また、oneofという修飾句があり、複数のクラスを含むような定義をしたあと、その中の「どれか一つだけが実際に入っている」という状態のデータを表現できる。

oneof accessor

https://developers.google.com/protocol-buffers/docs/proto3#oneof


これは大変便利で、


oneofSample.proto

message DataA { ... }

message DataB { ... }


message Box {

oneof dataBox {

DataA dataA = 1;

DataB dataB = 2;

}

}


とか作っておくと、

ネットワーク越しにbyte[]を受け取る -> とりあえずadtaBoxでDeserializeする -> dataBoxのタイプを判定、DataAかDataBのどちらかが入っている、というのが、次のようなコードでわかる

(C#)

byte[] messageBytes; // ネットワークから受け取ったデータが入ってると思って。


var box = new Box();

((IMessage)box).MergeFrom(messageBytes);


switch (box.MsgCase)

{

    case Box.MsgOneofCase.DataA:

        {

            var dataA = box.dataA;

.....


            break;

        }

    case Box.MsgOneofCase.DataB:

        {

            var dataB = box.dataB;

.....


            break;

        }


みたいな感じに、switchで中身を把握することができたりする。

これは一つの経路で複数のデータを扱う時に重宝する。


送る側は単にboxを作って好きなデータを一種だけ入れればよい。簡単。



使い方


1.protobufのライブラリをマシンにインストールする

mac: homebrewが入っていれば、

brew install protobuf


homebrew 何? って人は https://brew.sh


Windows: chocolateが入っていれば、

choco install protoc


chocolate 何?って人は https://chocolatey.org


これはコマンドラインツールになっていて、インストール後であればどこからでもprotocコマンドが呼べる、、はず。



2. .protoファイルからコードを生成する

protoc --csharp_out=. --proto_path=. protocol.proto


--csharp_out オプションで、出力する言語をC#にセット、パスを現在のパスに指定、

--proto_path オプションで、import(他のprotoファイルの参照)を探すパスを現在のパスに指定、

使用するprotoファイル名を記述、 というような感じ。


これで、protoファイルがあるのと同じパスに指定した言語のコードが生成される。



ここまでの工程で、C#で使えるSerializer/Deserializerのコードが自分のライブラリで使えるようになる。 簡単。


Unityでの一手間

IL2CPPの都合として、実行時にReflection.Emitが使えない、というものがある。

で、protobufはモロにこれを、byte[]からインスタンスを作る過程で、生成するインスタンスの型情報を取得するために使う。


2018年4月くらいにprotobufのC#コードに手が入り、使用したいデータ型をあらかじめコードに書くことでReflection.Emitが発生しないようになる、

= Unity + IL2CPPで使えるようになる、というメソッドが追加された。


using Google.Protobuf.Reflection;


FileDescriptor.ForceReflectionInitialization<データ型>();

この1行で、指定したデータ型を、IL2CPP環境で実行しても問題が発生しないようにできる。


ちなみにoneofを使っているデータ型を指定するとその内部のデータ型もすべてセーフになる。やったぜ平和。