yotiky Tech Blog

とあるエンジニアの備忘録

Unity (HoloLens) - MessagePack for C# の基本的な使い方

Unity (HoloLens) で使用するための MessagePack for C# のサンプルコード集です。

目次

開発環境

  • Unity : 2019.3.15f.1
    • Scripting Backend:IL2CPP
    • Platform:UWP ARM64
  • Visual Studio : 2019
  • MessagePack for C#:2.1.143
  • HoloLens 2

コードはこちらのリポジトリにあります。

https://github.com/yotiky/Sample.NetworkClient/blob/master/Assets/Scripts/BinarySerializer/MessagePackSamples.csgithub.com

導入

リポジトリこちらです。

リポジトリリリースから unitypackage をダウンロードします。 mpc とAnalyzer は任意でダウンロードしてください。

unitypackage を Unity プロジェクトにインポートします。

以前はUWPだと AutomataDictionary でエラーが出ていましたが既に解消されているようです。何の問題もなくビルド可能です。

通信で利用する場合は、サーバーサイドに Unity の基本型が存在しないので、 MessagePack と一緒に Vector3 などの定義が含まれる MessagePack.UnityShims をインストールしてください。

f:id:yotiky:20200702143401p:plain

サンプル

基本

まずは基本の使い方です。

シリアライズ対象のクラスには MessagePackObject 属性、プロパティには Key 属性を付ける必要があります。

Key の引数に int を与えると 出力順、Order になります。また、Key が重複していると、Generator を実行する際にエラーが発生するので重複しないようにしましょう。

    [MessagePackObject]
    public class PersonBasic
    {
        [Key(0)]
        public int Id { get; set; }
        [Key(1)]
        public AddressBasic[] Addresses { get; set; }
        }
    }
    [MessagePackObject]
    public class AddressBasic
    {
        [Key(2)]
        public string TelephoneNumber { get; set; }
        [Key(0)]
        public int Zipcode { get; set; }
        [Key(1)]
        public string Address { get; set; }
    }

シリアライズとデシリアライズの実装例です。

    var target = new PersonBasic
    {
        Id = 0,
        Addresses = new AddressBasic[]
        {
            new AddressBasic{ Zipcode = 1002321, Address = "hoge", TelephoneNumber = "0120198198" },
            new AddressBasic{ Zipcode = 1008492, Address = "fuga", TelephoneNumber = "0120231564" },
        },
    };
    var serialized = MessagePackSerializer.Serialize(target);
    var deserialized = MessagePackSerializer.Deserialize<PersonBasic>(serialized);

Json に変換をかけて出力した結果が次になります。TelephoneNumber が3つ目になっているのが分かります。

[0,[[1002321,"hoge","0120198198"],[1008492,"fuga","0120231564"]]]

デバッグ向けの便利なメソッド

MessagePack for C# には、デバッグ用途で便利なメソッドが用意されています。

ConvertToJson メソッドは、シリアライズ結果のバイナリを Json 文字列に変換してくれます。

    var json = MessagePackSerializer.ConvertToJson(serialized);

ConvertFromJson メソッドは、逆に Json 文字列をバイナリにシリアライズします。

    var serializedFromJson = MessagePackSerializer.ConvertFromJson(json);

SerializeToJson メソッドは、オブジェクトを Json 文字列にシリアライズします。

    var jsonFromDeserialized = MessagePackSerializer.SerializeToJson(deserialized);

上記メソッドを利用した例です。

    var serialized = MessagePackSerializer.Serialize(target);
    var json = MessagePackSerializer.ConvertToJson(serialized);
    Debug.Log(json);

    var serializedFromJson = MessagePackSerializer.ConvertFromJson(json);
    var deserialized = MessagePackSerializer.Deserialize<PersonBasic>(serializedFromJson);
    Debug.Log(MessagePackSerializer.SerializeToJson(deserialized));

json 変数の出力結果です。

[0,[[1002321,"hoge","0120198198"],[1008492,"fuga","0120231564"]]]

deserialized 変数の出力結果です。全く同じになります。

[0,[[1002321,"hoge","0120198198"],[1008492,"fuga","0120231564"]]]

属性

Key 属性の引数に文字列を渡すと、文字列がキーとなります。 Json に変換する際は別名にもなります。IgnoreMember 属性をつけるとシリアライズ処理の対象外になります。

    [MessagePackObject]
    public class AddressSample1
    {
        [Key("Foo")]
        public int Zipcode { get; set; }
        [Key("Bar")]
        public string Address { get; set; }
        [IgnoreMember]
        public string TelephoneNumber { get; set; }
    }

Json に変換して出力すると次のようになります。

{"Foo":100,"Bar":"hogehoge"}

また、クラスに付与する MessagePackObject 属性の引数 keyAsPropertyNametrue を渡すと、プロパティに属性をつけなくてもシリアライズできるようになります。

    [MessagePackObject(true)]
    public class AddressSample2
    {
        public int Zipcode { get; set; }
        public string Address { get; set; }
    }

Json.NETのように属性をつけないクラスをシリアライズする手段も用意されています。しかし Editor では実行可能ですが、IL2CPP環境下では Generate されていないクラスは失敗するようです。

    public class AddressSample3
    {
        public int Zipcode { get; set; }
        public string Address { get; set; }
    }

シリアライズとデシリアライズの実装例です。 Serialize メソッドと Deserialize メソッドの第2引数に MessagePack.Resolvers.DynamicObjectResolverAllowPrivate.Options を渡します。

    var serialized = MessagePackSerializer.Serialize(target, MessagePack.Resolvers.ContractlessStandardResolver.Options);
    Debug.Log(MessagePackSerializer.ConvertToJson(serialized));

    var deserialized = MessagePackSerializer.Deserialize<AddressSample3>(serialized, MessagePack.Resolvers.ContractlessStandardResolver.Options);
    Debug.Log(MessagePackSerializer.SerializeToJson(deserialized, MessagePack.Resolvers.ContractlessStandardResolver.Options));

同じように、第2引数に MessagePack.Resolvers.DynamicObjectResolverAllowPrivate.Options を渡すことで プライベートメンバーにも対応可能なようです。こちらはIL2CPP環境下で動くかは未確認です。

インターフェイス

MessagePack for C# ではインターフェイスを扱うことが可能です。

対象となるインターフェイスには、Union 属性で具象クラスを指定する必要があります。

    [MessagePack.Union(0, typeof(FooClass))]
    [MessagePack.Union(1, typeof(BarClass))]
    public interface IUnionSample
    {
    }
    [MessagePackObject]
    public class FooClass : IUnionSample
    {
        [Key(0)]
        public int Zipcode { get; set; }
    }
    [MessagePackObject]
    public class BarClass : IUnionSample
    {
        [Key(0)]
        public string Address { get; set; }
    }

次にシリアライズとデシリアライズの実装例です。

    IUnionSample target = new FooClass { Zipcode = 400 };
    var serialized = MessagePackSerializer.Serialize(target);
    Debug.Log(MessagePackSerializer.ConvertToJson(serialized));

    var deserialized = MessagePackSerializer.Deserialize<IUnionSample>(serialized);
    // C# 7が使えない場合は、as か インターフェイスに識別子を持たせるなどして判定する
    switch (deserialized)
    {
        case FooClass x:
            Debug.Log(x.Zipcode);
            break;
        case BarClass x:
            Debug.Log(x.Address);
            break;
        default:
            break;
    }

Stream

MemoryStream を使った実装例です。 Stream に連続で書き込むことが可能です。デシリアライズでも順番に取り出すことができます。 ただし、Stream を ToArray() した byte[] を Json に変換かけてみましたが、2つ書き込んだうち最初の1個しか出力されませんでした。原因は追っていません。

      var target1 = new AddressSample1
    {
        Zipcode = 500,
        Address = "hoge"
    };
    var target2 = new AddressSample2
    {
        Zipcode = 600,
        Address = "fuga",
    };
    using (var stream = new MemoryStream())
    {
        MessagePackSerializer.SerializeAsync(stream, target1).Wait();
        MessagePackSerializer.SerializeAsync(stream, target2).Wait();
        // 最初に書き込んだ target1 しか出力されない
        Debug.Log(MessagePackSerializer.ConvertToJson(stream.ToArray()));

        stream.Position = 0;
        var deserializedFromStream1 = MessagePackSerializer.DeserializeAsync<AddressSample1>(stream).Result;
        Debug.Log(MessagePackSerializer.SerializeToJson(deserializedFromStream1));
        var deserializedFromStream2 = MessagePackSerializer.DeserializeAsync<AddressSample2>(stream).Result;
        Debug.Log(MessagePackSerializer.SerializeToJson(deserializedFromStream2));
    }

出力結果です。

// deserializedFromStream1 
{"Foo":500,"Bar":"hoge"}

// deserializedFromStream2 
{"Zipcode":600,"Address":"fuga"}

etc

MessagePack for C# では dynamic にデシリアライズすることができます。dynamic を使うことでインデクサでのアクセスが可能になります。がこれもIL2CPPでは動くか不安が残ります。(未確認)

実行

Generate

ランタイムコードの生成が禁止されている Unity IL2CPP で動かすには、MessagePack.Generator を使って事前にシリアライズ対象を生成して登録しておく必要があります。

Generator の実行方法を3つ紹介しておきます。

  1. リポジトリからダウンロードした mpc.zip を使う
  2. dotnet tool を使う、ついでにマニフェストでパッケージを管理する
  3. エディタ拡張から実行する

a はネイティブの実行ファイルが含まれるのでそれぞれの環境にあったファイルに、オプションを指定して実行します。

b は dotnet tool を使ってインストールする方法です。マニフェストを作るとプロジェクトでパッケージの管理ができるようになりそうです。

c は unitypackage に同梱されているエディタ拡張です。 Window > MessagePack > CodeGenerator で MessagePack CodeGen ウィンドウを開けます。

f:id:yotiky:20200623100413p:plain:w350

必要なランタイムや mpc がまだインストールされていない場合はその旨が表示されます。mpc をインストールすると下記のコマンドが実行されます。マニフェストは作成されません。

dotnet tool install --global messagepack.generator

global を指定した場合のデフォルトのインストール先は、C:\Users\YOUR_USER_ID\.dotnet\tools になります。*1

実行ファイルのオプションを設定して実行できるようになっています。 必須項目は -i-o です。Path を指定する場合は、Assetディレクトリがルートになります。Unity プロジェクトで読み取る必要があるため、Asset ディレクト内のどこかに出力するのがおすすめです。

必須項目のみの設定例を次に示します。

f:id:yotiky:20200623100517p:plain:w350

Register

アプリケーションの初期化処理など最初の方に実行されるコードのあたりで 作成したResolverと必要なResolver を登録する処理を追加します。 下記は公式にかかれているfull sample codeです。RuntimeInitializeOnLoadMethod 属性で実行されるようになっているので、特に呼び出したりしなくてもクラスを定義するだけで動きます。

using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public class Startup
{
    static bool serializerRegistered = false;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Initialize()
    {
        if (!serializerRegistered)
        {
            StaticCompositeResolver.Instance.Register(
                 MessagePack.Resolvers.GeneratedResolver.Instance,
                 MessagePack.Resolvers.StandardResolver.Instance
            );

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

            MessagePackSerializer.DefaultOptions = option;
            serializerRegistered = true;
        }
    }

#if UNITY_EDITOR


    [UnityEditor.InitializeOnLoadMethod]
    static void EditorInitialize()
    {
        Initialize();
    }

#endif
}

StandardResolver には、BuiltinResolver、AttributeFormatterResolver、UnityResolverが含まれます。 (StandardResolver で PrimitiveObjectResolver のFormatter を読み込んでいるので PrimitiveObjectResolver はいらない、、という判断でよいのかな?)

シリアライズ対象のクラスに変更を加えると毎回 Generator を実行する必要があるため、ビルド前処理などに仕込むと良いかもしれません。

参考