MacのUnityでMessagePack-CSharpのAOT Code Generationが動かない場合の対処

## MessagePackってmacで使えないんでしょ？　やだー！

[MessagePack-CSharp](github.com/neuecc/MessagePack-CSharp/)
存在はけっこう前から知っていたのですが、`Unity`の`IL2CPP`環境下だと事前コードジェネレート[^pre]が必要で、それがwindows環境でしか動かない……とのことだったので敬遠していました。

[MessagePack for C# v2によるC#における最新のI/Oパイプライン最適化](http://neue.cc/2019/12/17_586.html)

ところがv2が出てなんかmacでも動くようになった？　っぽい？　ということなので試してみました。

実際動いたやり方は[ここ](#コマンドラインで実行してみる)と[ここ](#エディタ拡張)に書いてあります。
おま環疑惑がだいぶ濃厚なので、そこのところを念頭に置いてお読みください。

## 事前説明
MessagePackのバージョンは以下のとおりです。
`2.0.335`

## データクラス
このデータクラスを`Serialize/Deserialize`するのが今回の目標です。
こんな感じのディレクトリに配置してあります。
<img width="147" alt="スクリーンショット 2020-01-29 0.58.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/4d595170-2611-fe7d-8514-9901dc60955c.png">


```c#:LineData.cs
using MessagePack;
using UnityEngine;

[MessagePackObject]
public class LineData
{
    [Key(0)]
    public int Index;

    [Key(1)]
    public string ID;

    [Key(2)]
    public Vector3[] Pos;

    [Key(3)]
    public Quaternion[] Rot;


    public override string ToString()
    {
        return $"{nameof(Index)}: {Index}, {nameof(ID)}: {ID}, {nameof(Pos)}: {Pos}, {nameof(Rot)}: {Rot}";
    }
}
```

### テストコード

```c#:testCode
[SerializeField]
private Text _text;

private void Start()
{
    StaticCompositeResolver.Instance.Register(
        StandardResolver.Instance,
        UnityBlitResolver.Instance,
        GeneratedResolver.Instance // 今回事前生成しようとしているResolver
    );

    var option = MessagePackSerializerOptions.Standard.WithResolver(StaticCompositeResolver.Instance);
    MessagePackSerializer.DefaultOptions = option;

    var before = new LineData();
    before.ID = Guid.NewGuid().ToString();
    before.Index = 1;
    before.Pos = Enumerable.Range(0, 10).Select(i => UnityEngine.Random.insideUnitSphere).ToArray();
    before.Rot = Enumerable.Range(0, 10).Select(i => UnityEngine.Random.rotation).ToArray();

    Debug.Log($"before = {before.ToString()}");

    var binary = MessagePackSerializer.Serialize(before);

    var after = MessagePackSerializer.Deserialize<LineData>(binary);

    Debug.Log($"after = {after.ToString()}");

    _text.text = before.ToString() + Environment.NewLine + after.ToString();
}
```

事前生成なしでもエディタでは動きますが、`Android`とかにビルドすると動きません。

## みちすじ
### 初回起動
`Window -> MessagePack -> CodeGenerator`

<img width="318" alt="スクリーンショット 2020-01-28 21.04.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/e60ce778-181b-0018-342b-69f57d37d332.png">
ねえぞ！　って言われます。
確かに`.Net Core 3`入れた覚えはないのでリンクの通りにインストールします。
`Terminal`からバージョンチェック。[^zsh]

```zsh:バージョン確認
dotnet --version
3.1.101
```

入ってます。というわけでもう一回起動。
<img width="318" alt="スクリーンショット 2020-01-28 21.04.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/e60ce778-181b-0018-342b-69f57d37d332.png">
ねえぞ！　って言われます。

### ある！！！！！！
`Assets/Scripts/MessagePack/Unity/MessagePackWindow.cs`
メッセージを生成しているのはこのスクリプトです。
ねえぞ！　って言ってきてるのはこのあたり。

```c#:MessagePackWindow_259~270
public static async Task<(bool found, string version)> FindDotnetAsync()
{
    try
    {
        var version = await InvokeProcessStartAsync("dotnet", "--version");
        return (true, version);
    }
    catch
    {
        return (false, null);
    }
}
```

`InvokeProcessStartAsync`の中身を見る限り、普通に`Terminal`で`dotnet --version`と入力したときと同じ結果を返してくる……はず。再起動してみても変化はありません。
`ProcessStartInfo`の`FileName`とか`WorkingDirectory`とかがうまくいってないっぽいですが、シェルにぜんぜん詳しくないのでわかりません。

### 気分転換
いったんUnityを離れてリポジトリのReadMeの手順を進めます。`MessagePack.Generator`をインストール。
`dotnet tool install --global MessagePack.Generator`

入ってます。でなんかdotnet toolsをPATHに追加しろとか言ってくるので素直に表示されるコマンドをコピー→実行していきます。今回も`Terminal`から存在チェック。

```zsh:CodeGenerator
dotnet-mpc
argument list:
-i, -input: Input path of analyze csproj or directory, if input multiple csproj split with ','.
-o, -output: Output file path(.cs) or directory(multiple generate file).
-c, -conditionalSymbol: [default=null]Conditional compiler symbols, split with ','.
-r, -resolverName: [default=GeneratedResolver]Set resolver name.
-n, -namespace: [default=MessagePack]Set namespace root name.
-m, -useMapMode: [default=False]Force use map mode serialization.
-ms, -multipleIfDirectiveOutputSymbols: [default=null]Generate #if-- files by symbols, split with ','.
```

います。でもどうせねえぞって言われるんでしょ。知ってる。

### コマンドラインで実行してみる
`/Users/XXXXXX/.dotnet/tools/dotnet-mpc`
実態っぽいのはここのパスにあります。なのでAssets直下に移動して次のコマンドを打ってみます。
`/Users/XXXXXX/.dotnet/tools/dotnet-mpc -i Scripts/Entity -o Scripts/Entity/Generated/MessagePackGenerated.cs`

<img width="220" alt="スクリーンショット 2020-01-29 1.03.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/8d321ca8-a510-e4f3-92bf-d797613d2503.png">

こんな感じで生成されました。
`MessagePackGenerated.cs`には`using UnityEngine`が足りなくてエラーが出ていますが、それさえ補填してあげればビルドは通ります。
ビルドすると実機でも動作します。

### エディタ拡張
こんな感じのメソッドを作ってみました。とりあえず自分の環境では動いてます。
既存のエディタ拡張をいろいろコメントアウトするとか、必要なとこだけコピペして新しく作るとかしてこのメソッドに食わせれば動く……はず。
`using UnityEngine`が足りなくてエラー出てるのは相変わらずなのでこわいですが。

```c#:GenerateResolverAsync
public static Task<string> GenerateResolverAsync(MpcArgument argument)
{
    var psi = new ProcessStartInfo()
    {
        CreateNoWindow = true,
        WindowStyle = ProcessWindowStyle.Hidden,
        StandardOutputEncoding = Encoding.UTF8,
        StandardErrorEncoding = Encoding.UTF8,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".dotnet/tools/dotnet-mpc"),
        Arguments = argument.ToString(),
        WorkingDirectory = Application.dataPath
    };

    Process p;
    try
    {
        p = Process.Start(psi);
    }
    catch (Exception ex)
    {
        return Task.FromException<string>(ex);
    }

    var tcs = new TaskCompletionSource<string>();
    p.EnableRaisingEvents = true;
    p.Exited += (object sender, System.EventArgs e) =>
    {
        var data = p.StandardOutput.ReadToEnd();
        p.Dispose();
        p = null;

        tcs.TrySetResult(data);
    };

    return tcs.Task;
}
```

## まとめ
macでもなんとかならないことはないっぽい。です。
windowsでは試していませんが、あっさり動くのではないでしょうか。
ぶっちゃけ`Serialize/Deserialize`するクラスを頻繁に変更することってそうないと思うので、必要なときだけwindowsで生成してgitで共有してもらったほうが早いと思います。

ちなみにOSアップデートしてない、まだ`bash`のmacでも同様でした。とはいえどちらも自分が使っているやつなので、他の人が使ってるmacだと動くかも。
あと、ぱっと見た感じIssuesに似たような問題は上がってませんでした。おま環なのでは疑惑。


おしまい。


## 参考
[MessagePack-Csharp と MagicOnion のコード生成を自動化する](https://qiita.com/p_a_sta/items/4cfd001cd77e0c2039a9)


[^pre]: コンパイル時にコードを生成しておく、のが大好きです。もっと言うなら通信とかするなら`ProtoBuf`みたいにお約束を作っておいたほうがみんなしあわせなのでは？　くらいに思っています。でも`.proto`書くのめんどくせえ！
[^zsh]: なんか知らないうちに`bash`じゃなくて`zsh`になってる。macのOSアップデートでそうなった？　らしい？　なぞ。