環境
- OSX 10.11.5
- Ruby 2.2.5
- .Net 2.0 (Unity)
- Unity 5.4
- mono 4.4.1
目的
クライアント側でJSONのパーサやレコードクラスの定義を手で書くのを避ける。
JSON に含まれるデータの型を明示する。
(long のつもりが int になっててオーバーフローしてしまった、とかいう悲惨な事故を起こさない)
前提条件
本稿では avro-builder は rails アプリ から利用する事を前提とします。
また C# の IDL は Unity 開発で利用する事としています。
ツール選定
要件
- データを JSON で扱える
- クライアントでの型を明示できる
- C# のランタイムがある
- C# のレコードクラスを生成してくれる
- Unity で使える
- iOS / Android の IL2CPP ビルドで動作する
- これによるサーバ側の作業を要さない
候補
- Protocol Buffer
- Avro
次点で下記も候補に上がったが、IDL 生成までは公式ではサポートされていない。
- Msgpack
- NetSerializer
結果
Avro
選定理由
Avro
対応する言語が潤沢で、ツールも充実していた。
JIT コンパイルしている箇所があったが、局所的なので修正して使えそうだった。
Protocol Buffer
今回の開発環境が Unity でなければ採用したかった。
公式のC#バインディングが Unity 対応していない。
非公式のバインディングでは protobuf-net が存在するが、更新状況が芳しくない。
Unity 対応については issue に上がっているが・・・
@amlinux @zhangzhibin I do understand the Unity support problems, since I'm working with Unity daily since many years, but I do agree with @jskeet, I believe it is not up to the world to adjust to Unity, but Unity to upgrade to a modern version of .NET, people just can't keep living in the middle-age because of Unity...
個人的にはこの意見に賛成。
手順
- DSL(Ruby) スキーマを用意する
- DSLスキーマをAvroスキーマ(.avsc) に変換
- .avsc から C# の IDL 生成
- 組み込み
- AOT 対応
用意するもの
- avro-builder
- Avro
- avro-tools
1. DSL(Ruby) スキーマを用意する
任意の名称で、 ${RAILS_APP_ROOT}/avro/dsl 以下に配置します。
サーバ側の作業を増やさないことを念頭にしていますが、クライアント側で各データをどのような型で扱うべきかは最低限定義する必要があります。
実際は手で定義するなり、既存の Model や Selializer から動的生成するなどしましょう。
本稿では下記スキーマを例として利用します。
namespace 'example.avro.smith'
record :user do
required :id, :long
required :user_name, :string
end
record :team do
required :leader, :user
required :members, :array, items: :user
end
2. DSLスキーマをAvroスキーマ(.avsc) に変換
事前に Gemfile に avro-builder を追加して、アプリで利用できるようにしておきます。
gem 'avro-builder'
avro-builder が提供する rake タスクを実行します。
$ rake avro:generate
この rake タスクでは ${RAILS_APP_ROOT}/avro/dsl 以下を走査して .avsc を生成します。
.avsc は ${RAILS_APP_ROOT}/avro/schema 以下に生成されます。
中身はスキーマを JSON で表現したものです。
3. .avsc から C# の IDL 生成
Avro 本体に付属するコードジェネレータを利用します。
はじめに git からクローンしてきたソースをビルドします。
$ cd ${AVRO_DIR}
$ ./build.sh dist
その後、ビルドされたコードジェネレータを実行します。
オプションは、スキーマファイルと出力先の指定です。
$ mono ./lang/csharp/build/codegen/Release/avrogen.exe -s ./avro/schema/schema.avsc ./avro/idl
出力されたファイル名は Win 準拠になっているため、バックスラッシュで区切った出力先のパスが含まれます。
これを適宜修正します。
4. 組み込み
avrogen.exe で生成した .cs ファイルを Unity プロジェクトの Assets 以下の任意の場所に配置します。
動作を確認するためにランダムなデータを生成します。
データ生成は avro-tools で実行可能です。
avro-tools は公式で jar が提供されていますが、本稿では OSX を前提としているため brew でインストールします。
$ brew install avro-tools
ランダムデータ生成。
$ avro-tools random ./avro/random.avro --schema-file ./avro/schema/schema.avsc --count 20 --codex null
出力した .avro を StreamingAssets 以下に配置します。
codegen から吐き出されたクラスから、正しくデータが取得できていることを確認します。
string url = Application.streamingAssetsPath + "/Avro/random.avro";
# if UNITY_IOS || UNITY_EDITOR
WWW www = new WWW("file://" + url);
# else
WWW www = new WWW(url);
# endif
while (!www.isDone) {}
using (MemoryStream stream = new MemoryStream(www.bytes)) {
IFileReader<team> dataFileReader = DataFileReader<team>.OpenReader(stream);
team t = null;
while (dataFileReader.HasNext()) {
t = dataFileReader.Next();
Debug.Log("t.leader.id : " + t.leader.id);
Debug.Log("t.leader.user_name : " + t.leader.user_name);
for (int i = 0; i < t.member.Count; i++) {
user u = t.member[i];
Debug.Log("t.member[" + i + "].id : " + t.member[i].id);
Debug.Log("t.member[" + i + "].user_name : " + t.member[i].user_name);
}
}
}
5. AOT 対応
上記で Android 及び UnityEditor では動作させることができます。
ただし Avro ランタイム中に Reflection.Emit の使用箇所があるため、 JIT の許容されていない iOS ではエラーとなり動作しません。
そのため、 Avro ランタイムのコードを修正する必要があります。
差分は下記 PR にて参照することができます。
https://github.com/apache/avro/pull/134
PR 元のブランチはこちらです。
https://github.com/dolow/avro/tree/feature/aot_support
なお、現時点ではマージされておらず、今後取り込まれる保証もないため、導入は自己責任において行ってください。
2年の時を経てマージされたのを、さらにその2年後に気付きました、やったね。
以上です。