Avro - Ruby でスキーマを作って C# 用のIDLを出力して Unity で利用する

# 環境

- 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](https://github.com/mgravell/protobuf-net) が存在するが、更新状況が芳しくない。

Unity 対応については [issue](https://github.com/google/protobuf/issues/644) に上がっているが・・・

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

個人的にはこの意見に賛成。

# 手順

1. DSL(Ruby) スキーマを用意する
2. DSLスキーマをAvroスキーマ(.avsc) に変換
3. .avsc から C# の IDL 生成
4. 組み込み
5. AOT 対応

__用意するもの__

- [avro-builder](https://github.com/salsify/avro-builder)
- [Avro](https://github.com/apache/avro)
- avro-tools

## 1. DSL(Ruby) スキーマを用意する

任意の名称で、 ${RAILS_APP_ROOT}/avro/dsl 以下に配置します。
サーバ側の作業を増やさないことを念頭にしていますが、クライアント側で各データをどのような型で扱うべきかは最低限定義する必要があります。
実際は手で定義するなり、既存の Model や Selializer から動的生成するなどしましょう。
本稿では下記スキーマを例として利用します。

```schema.rb
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 を追加して、アプリで利用できるようにしておきます。

```Gemfile
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年後に気付きました、やったね。

----

以上です。