MessagePackハッカソン #3が開催され、Bashoで場所を提供したので僕も久しぶりになんかやることにして、そういえばmsgpack-erlangの新仕様の実装が途中だったのでやることにした。で、できた。というわけで、さまざまな事件を乗り越えて策定された新仕様をErlangのサンプルコードつきで解説しよう。新仕様の目玉はふたつあるので、それぞれ解説しておきたい。
文字列型
これまでMessagePackは「ほぼJSON互換」だったのだが、その正体は、Cらしくバイナリ型(raw)を <<101XXXXX>> とか 0xDA, 0xDB で表現していた。一方JSONにはバイナリ型はなくて、文字列はUnicodeでなければならないので、生のバイナリをそのままJSONに持っていくことはできなかった。で、新しい仕様ではこれまでバイナリ型に割り当てられていたところを含めて、新しく文字列型をつくった( <<101XXXXX>>, 0xD9, 0xDA, 0xDB)。新仕様では、バイナリ型には新しく 0xC4, 0xC5, 0xC6 が割り当てられた。
で、ライブラリを作る立場としては、まあ最初はオプショナルでこれを実装しようということになる。使い方は簡単で、 enable_str というオプションを一緒に渡してやるだけ(公式のドキュメントは間違っている。私がまだ修正していない)。
{ok, Bin} = msgpack:pack("埼玉", [{enable_str=true}]), {ok, "埼玉"} = msgpack:unpack(Bin, [{enable_str=true}]).
ちなみに、デフォルトではこれは false になっていて、引数を省略したらどちらもこれまでと同様の動作をする。また、これを true にした場合は変換を試みるので自動的に Unicode のバリデーションは走ることになる。失敗するとコケるはず。確か。
拡張型
MessagePackがサポートするのは配列とかMap型とか整数とかそういったプリミティブな型ばかりだが、使っているうちにもうちょっとリッチな型というものがほしくなる。たとえば日付、時刻、その他オレにとっては便利な○○…文字列も初めはそうだったのだが。基本方針は以前と同様「ユーザー側でMessagePack上で好きなように規約やライブラリを作ってやってよー(バイナリレベルではやらない)」だったのだけど、やはり効率重視なのでシリアライザの層でちょっとは効率的にできるようになってほしい。というわけで、ユーザー定義の拡張型を仕様として用意することにした。ライブラリはまだ全然できてないので注意。で、Erlangではユーザー定義型のシリアライザとデシリアライザを関数として渡してやる。
Packer = fun({ref, Ref}, Opt) when is_ref(Ref) -> {ok, {12, term_to_binary(Ref)}} end, Unpacker = fun(12, Bin) -> {ok, {ref, binary_to_term(Ref)}}, Ref = make_ref(), Opt = [{ext,{Packer,Unpacker}}], {ok, {ref, Ref}} = msgpack:unpack(msgpack:pack({ref, Ref}, Opt), Opt).
上の例だと、Erlangではよく使う参照(UUIDのようなもの)はこれまでのMessagePackではシリアライズできなかったのだが、拡張型を使ってコード(ここでは12)を割り当てることができるようになる。これで、すべてのErlang組み込み型がシリアライズできるようになるはず(もう少ししたら便利ライブラリを付け足すはず…)で、BERTやt2bでは多言語とのやりとりが大変だったけど、MessagePackと使い分けるのは面倒だったごく一部の人には朗報かと思います。
あと、拡張型を扱うためのbehaviour (msgpack_ext) も作った。ユーザーの好きなモジュールで、 pack_ext と unpack_ext を実装してbehaviourを定義すればatomを渡すだけで使えるようになる。
-behaviour(native_example). pack_ext({native, Term}, _) when is_pid(Term) orelse is_reference(Term) orelse is_port(Term) orelse is_tuple(Term) orelse is_function(Term) -> {ok, {42, term_to_binary(Term)}}. unpack_ext(42, Bin) -> {ok, {native, binary_to_term(Bin)}}.
とかしておけば、
{ok, Bin} = msgpack:pack({native, make_ref()}, [{ext, native_example}]),
とすれば拡張型を使えるようになる。もちろん、既存のリストやMap型に入れ込んでも普通にデコードされる(はず…)。まだproperで網羅的なテストは書けていないので、有志のヘルプ求む。というか、この記事は普通にどっかに英語で書くべきだろうな…
Let's Do MessagePack!!