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 をインストールしてください。
サンプル
基本
まずは基本の使い方です。
シリアライズ対象のクラスには 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
属性の引数 keyAsPropertyName
に true
を渡すと、プロパティに属性をつけなくてもシリアライズできるようになります。
[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つ紹介しておきます。
a はネイティブの実行ファイルが含まれるのでそれぞれの環境にあったファイルに、オプションを指定して実行します。
b は dotnet tool を使ってインストールする方法です。マニフェストを作るとプロジェクトでパッケージの管理ができるようになりそうです。
c は unitypackage に同梱されているエディタ拡張です。 Window > MessagePack > CodeGenerator で MessagePack CodeGen ウィンドウを開けます。
(MessagePack 2.2.60 時点で動かなくなっています。対処法の記事はこちら)
必要なランタイムや mpc がまだインストールされていない場合はその旨が表示されます。mpc をインストールすると下記のコマンドが実行されます。ローカルツールのマニフェストは作成されません。
dotnet tool install --global messagepack.generator
global を指定した場合のデフォルトのインストール先は、C:\Users\YOUR_USER_ID\.dotnet\tools
になります。*1
実行ファイルのオプションを設定して実行できるようになっています。
必須項目は -i
と -o
です。Path を指定する場合は、Assetディレクトリがルートになります。Unity プロジェクトで読み取る必要があるため、Asset ディレクト内のどこかに出力するのがおすすめです。
必須項目のみの設定例を次に示します。
-i : ..\Assembly-CSharp.csproj
-o : Scripts\MessagePackGenerated.cs
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 を実行する必要があるため、ビルド前処理などに仕込むと良いかもしれません。
参考
- neuecc/MessagePack-CSharp: Extremely Fast MessagePack Serializer for C#(.NET, .NET Core, Unity, Xamarin). / msgpack.org[C#]
- neue cc - MessagePack for C# v2によるC#における最新のI/Oパイプライン最適化
- neue cc - MessagePack for C#におけるオートマトンベースの文字列探索によるデシリアライズ速度の高速化
- neue cc - MessagePack for C# 1.4.1 - JSONサポート強化, dynamic対応, Typelessシリアライズなど
- 黒騎士と白の魔王におけるMessagePack-CSharpのUnionの活用事例 - Grani Engineering Blog
- スクリプトの制限 - Unity マニュアル