久しぶりの投稿です。
C#におけるMessagePackライブラリの中で、高速であるとされる MessagePack-CSharp。
高速で洗練されているものの、率直に申しますと、MessagePackの古い仕様のサポートが不十分で面倒なようです。その克服の方法です。
ちなみにこの克服にあたり、本家MessagePack-CSharpに修正のpull requestを出し、受理されました。
ただし旧仕様を使うなら、速度等にこだわりがないなら、古くから実績のある MessagePack for CLI (以下 MsgPack-Cli) を使うのが楽だと思います。 github.com
環境
- Windows 10
- Visual Studio Community 2017 Preview Version 15.4.0 Preview 6.0
- MessagePack-CSharp 1.7.0 (https://github.com/neuecc/MessagePack-CSharp/releases/tag/v1.7.0)
- MsgPack-Cli 0.9.2 (https://github.com/msgpack/msgpack-cli/releases/tag/0.9.2)
MessagePackの古い仕様による問題
(私は全然詳しくないのですが)
MessagePackの当初の仕様では、文字列型が存在せず、raw bytes
として扱われます。
https://github.com/msgpack/msgpack/blob/master/spec-old.md
その後、現在の仕様では文字列型が追加されました。 https://github.com/msgpack/msgpack/blob/master/spec.md
それにより、例えば古い仕様で raw 16
型 を表す0xda
は、新仕様では str 16
を表します。raw 32も同様です。古いMessagePackデータを新仕様のライブラリで解釈するとエラーになります。
MessagePack-CSharp でエラーになる例と、その解決法
以降では、MsgPack-Cliをリファレンス実装 (旧仕様のMessagePackを使う実装) として使用します。
byte[] 単独のデシリアライズ
まずはbyte配列1個のデシリアライズに挑戦。
// byte[] を旧仕様でシリアライズ byte[] sourceBytes = Enumerable.Repeat((byte)128, 1000).ToArray(); // 適当なbyte配列 byte[] messagePackBytes = SerializeByClassicMsgPack(sourceBytes); // MessagePack-CSharpでデシリアライズ byte[] deserializedBytes = MessagePackSerializer.Deserialize<byte[]>(messagePackBytes);
// MsgPack-Cli により、旧仕様でシリアライズして返す private static byte[] SerializeByClassicMsgPack<T>(T obj) { var context = new MsgPack.Serialization.SerializationContext { SerializationMethod = MsgPack.Serialization.SerializationMethod.Array, // 旧仕様でお願いします(超楽!) CompatibilityOptions = { PackerCompatibilityOptions = MsgPack.PackerCompatibilityOptions.Classic } }; var serializer = MsgPack.Serialization.MessagePackSerializer.Get<T>(context); using (var memory = new MemoryStream()) { serializer.Pack(memory, obj); return memory.ToArray(); } }
これを実行すると、str 16が云々と言われて例外が発生します。
当時かなり途方にくれましたが、これの解決には MessagePack.Formatters.OldSpecBinaryFormatter
を使います。
byte[] deserializedBytes = OldSpecBinaryFormatter.Instance.Deserialize( messagePackBytes, 0, StandardResolver.Instance, out var readSize);
クラスのプロパティとしてbyteがある例
上記で最低限の実装はどうにか書けます。しかし実際にはbyte単独でシリアライズすることはあまりなく、クラス単位で行うことがほとんどだと思います。
[DataContract] public class Foo { [DataMember] public int Id { get; set; } [DataMember] public byte[] Value { get; set; } }
// MsgPack-Cli により、旧仕様でシリアライズして返す private static byte[] SerializeByClassicMsgPack<T>(T obj) { var context = new MsgPack.Serialization.SerializationContext { SerializationMethod = MsgPack.Serialization.SerializationMethod.Map, // ここだけArrayから変更 CompatibilityOptions = { PackerCompatibilityOptions = MsgPack.PackerCompatibilityOptions.Classic } }; var serializer = MsgPack.Serialization.MessagePackSerializer.Get<T>(context); using (var memory = new MemoryStream()) { serializer.Pack(memory, obj); return memory.ToArray(); } }
var sourceBytes = Enumerable.Repeat((byte)128, 1000).ToArray(); // 適当なbyte配列 var foo = new Foo { Id = 123, Value = sourceBytes }; var messagePackBytes = SerializeByClassicMsgPack(foo); // crash Foo deserializedObj = MessagePackSerializer.Deserialize<Foo>(messagePackBytes);
上記も動きません。
MessagePack-CSharp 1.7.0 以降での回避
https://github.com/neuecc/MessagePack-CSharp/releases/tag/v1.7.0
Improve MessagePackFormatterAtribute supports property and field.
これを活用し、byte[]のシリアライズを特別扱いさせます。
public class OldSpecByteArrayFormatter : IMessagePackFormatter<byte[]> { public int Serialize(ref byte[] bytes, int offset, byte[] value, IFormatterResolver formatterResolver) { return OldSpecBinaryFormatter.Instance.Serialize(ref bytes, offset, value, formatterResolver); } public byte[] Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize) { return OldSpecBinaryFormatter.Instance.Deserialize(bytes, offset, formatterResolver, out readSize); } }
[DataContract] public class Foo { [DataMember] public int Id { get; set; } [DataMember] [MessagePackFormatter(typeof(OldSpecByteArrayFormatter))] // これが肝 // 注:typeof(OldSpecBinaryFormatter) と直接書きたいですが、1.7.0時点ではコンストラクタがinternalなので落ちます public byte[] Value { get; set; } }
var sourceBytes = Enumerable.Repeat((byte)128, 1000).ToArray(); // 適当なbyte配列 var foo = new Foo { Id = 123, Value = sourceBytes }; var messagePackBytes = SerializeByClassicMsgPack(foo); // 成功! Foo deserializedObj = MessagePackSerializer.Deserialize<Foo>(messagePackBytes);
MessagePack-CSharp 1.6.2 以前での回避
MessagePack-CSharp 1.7.0から、netstandard2.0
が必須になりました。しかし1.xが必要な場面で困るので、更新できないケースもあるかと思います。
(たとえば、AWS Lambda とか)
1.6.2以前ではMessagePackFormatterをフィールドに指定できないので、以下のような手を使います。
// MessagePack旧仕様対応シリアライザ内蔵の、byte[] をラップするオブジェクト [MessagePackObject] [MessagePackFormatter(typeof(Formatter))] public class OldSpecByteArray { [Key(0)] public byte[] Value { get; set; } // OldSpecByteArray と MessagePack の変換をするフォーマットクラス // 1.7.0向けで示した実装とほぼ同じ public class Formatter : IMessagePackFormatter<OldSpecByteArray> { public int Serialize(ref byte[] bytes, int offset, OldSpecByteArray value, IFormatterResolver formatterResolver) { return OldSpecBinaryFormatter.Instance.Serialize(ref bytes, offset, value.Value, formatterResolver); } public OldSpecByteArray Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize) { var value = OldSpecBinaryFormatter.Instance.Deserialize(bytes, offset, formatterResolver, out readSize); return new OldSpecByteArray { Value = value }; } } }
// Fooオブジェクトには byte[] の代わりに OldSpecByteArray を持たせる [DataContract] public class Foo { [DataMember] public int Id { get; set; } [DataMember] public OldSpecByteArray Value { get; set; } }