Skip to content

Instantly share code, notes, and snippets.

@frsyuki
Last active December 14, 2015 03:19
Show Gist options
  • Save frsyuki/5019759 to your computer and use it in GitHub Desktop.
Save frsyuki/5019759 to your computer and use it in GitHub Desktop.

msgpackの変更案について-3

概要:

  • 現行のRaw型をString型として読み替える。
    • 現行の FixRaw, raw 16, raw 32 は、FixString, string 16, string 32 になる
  • Binary型を新設する。
  • バイト列はBinary型で保存する。そうでなければString型で保存する。

背景

文字列とバイナリの区別が曖昧な言語が存在する。例えば:

  • 文字列とバイト列を区別する型がそもそも無い言語(PHP, C++, Erlang, OCaml)
  • 文字列を表すためにフラグ等の付加情報が使われるが、文字列なのに付加情報が付与されていないケースが一般的に存在する言語(Ruby, Perl, Python 2)

これらの言語を weak-string languages と呼ぶ。

一方で、区別が明確な dynamically-typed languages も存在する(JavaScript、Objective-C、Python 3)。また、区別が明確な statically-typed languages が存在する(Java、C#、Scala)。これらの言語を strong-string languages と呼ぶ。

strong-string and dynamically-typed languages が受信者となる オブジェクトのやりとりでは、文字列として送信/保存したデータを文字列として、バイト列として送信/保存したバイト列をバイト列として、透過的に復元したいという要求が存在する。(※この要求は、その他の組み合わせの通信では存在しない)

一方で、weak-string languages では、すべての文字列に対して「これは文字列である」とマーカを付与する、あるいはすべてのバイト列に「これはバイト列である」というマーカを付与する作業は手間がかかる。従ってこれらの言語で書かれたプログラムで、文字列とバイト列が明確に区別できることを期待することは現実的ではない。

そこで、次のメリットを同時に満たす変更を提案する:

  • strong-string languages 同士の通信においては、文字列を文字列として、バイト列をバイト列として透過的に復元できるようにする
  • weak-string languages と strong-string languages が混在した場合のデータ交換では:
    • weak-string languages においてすべてのバイト列にマーカを付ける作業を行えば、strong-string languages で透過的に型を復元できるようにする
    • すべての文字列ではなくすべてのバイト列にマーカを付けるのは、バイト列の方が文字列より数が少なく、その作業の方が簡単だという仮定に基づいている
    • そうでなくても、strong-string languages 側のアプリケーションで適切な実装を行えば、透過的ではないがデータ交換が行えるようにする
  • weak-string languages 同士の通信では、既存のmsgpackとの互換性を維持する

型システムの変更

  • Binary: バイト列
  • String: UTF-8でエンコードされた文字列
    • UTF-8としてinvalidなバイトシーケンスを含む文字列が保存されていることもある。この理由は次の3つ:
      • Unicodeは、その正しい取り扱いをmsgpackの全言語の実装にさせるには、複雑すぎるため
      • シリアライズ時にvalidateを行うと性能にインパクトがあるため(strong-string languages であっても、UTF-8としてinvalidなバイトシーケンスを含む文字列オブジェクトを簡単に作れる言語が存在することに注意;例:Python2)
      • weak-string languages で書かれたプログラムでは、文字列とバイト列が明確に区別できることを期待することは現実的ではなく、結果としてバイト列がStringとして扱われることがあるため
    • invalidなバイトシーケンスを含む文字列を検出した場合の動作は、実装に依存する
      • ただし、invalidなバイトシーケンスを含む文字列をどのように取り扱うかは、アプリケーションが決定するべき仕事である

UTF-8としてinvalidなバイトシーケンスを含むStringを受け取った場合の動作は実装に依存する。例外を発生させて弾く実装を行っても良いし、その場合に限りバイト列型を返すといった実装でも良い。しかし、UTF-8としてinvalidなバイトシーケンスを含むString型オブジェクトが保存されていたとしても、アプリケーションが望めば元のバイト列を取り出せるようにする手段も提供することが、強く推奨される(SHOULD or MUST)。

フォーマットの変更

0xa0-0xbf FixString (0bytes - 31bytes String type)  // changed

0xd5 binary 8 (0bytes - 255bytes Raw type)  // new
0xd6 binary 16 (256bytes - 65535bytes Raw type)  // new
0xd7 binary 32 (65536bytes - 4294967295bytes Raw type)  // new

0xd8 reserved

0xd9 string 8 (32bytes - 255bytes String type)  // new
0xda string 16 (256bytes - 65535bytes String type)  // changed from raw 16
0xdb string 32 (65536bytes - 4294967295bytes String type)  // changed from raw 32

strong-string and dynamically-typed languages における実装のガイドライン

  • シリアライザ:

    • バイト列はBinary型として保存する
    • 文字列はString型として保存する
    • ただし、既存の実装との互換性を維持するために、バイト列もString型として保存するオプションを実装しても良い
  • デシリアライザ:

    • String型またはBinary型をデシリアライズしたら、それと分かるオブジェクトを返す
    • String型にUTF-8としてinvalidなバイトシーケンスが含まれていた場合に、アプリケーションがそれらをハンドリングできるようにする機能を提供するべきである(SHOULD)
    • この実装方法は特に規定しない。次のような方法が考えられる:
      • invalidなバイトシーケンスを発見したら、そのオリジナルのバイト列をフィールドに持つオブジェクトのインスタンスを返す
      • invalidなバイトシーケンスを含むことができる文字列クラスを組み込み型とは別に作成し、invalidなバイトシーケンスが含まれるか否かに関わらず、常にそれを返すモードを実装する
      • invalidなバイトシーケンスを発見したら指定されていたコールバック関数を呼び出し、その関数の返値を返す

weak-string languages における実装のガイドライン

  • シリアライザ:
    • バイト列か文字列かが自明でなければ、String型として保存する
    • UTF-8のvalidationは行わなくてよい。String型はUTF-8としてinvalidなバイトシーケンスを許容する
    • ユーザーが明示的に「これはバイト列だ」とヒントを設定したオブジェクトを受け取った場合、それはBinary型として保存するべきである(SHOULD)
  • デシリアライザ:
    • String型のvalidationを行うべきではない(SHOULD NOT)
      • なぜなら、アプリケーションがinvalidなバイトシーケンスを含むStringの扱いを決めるべきだから
    • ただし、オプションを有効にすればString型でvalidationを機能を実装をしてもよい(MAY)
    • Binary型を受け取った場合、何らかのフラグが立った(String型とは区別できる情報を含む)オブジェクトを返すオプションを実装しても良い(MAY)
      • この挙動は、MessagePackを入力として、MessagePackを出力するような、中間処理を行うツールで、出力先でも型情報を維持しなければならないケースで必要になる
      • この機能が必要なケースは、稀である
      • 実装の方法には、例えばPHPにおいては、特定のオプションがonであったら、Binary型を受け取った場合に、バイト列をフィールドとして所有するMessagePackBinaryクラスのインスタンスを返す方法ある

既存の実装の互換性について

  • マイナーバージョンアップで、新設されるBinary型をバイト列として返す実装をリリースする
    • この時点で、新しいエンコーダとの互換性が達成される
  • メジャーバージョンアップで、バイト列を新設Binary型でシリアライズする実装をリリースする
    • 同時に、区別が厳格な環境のエンコーダにおいて、バイト列もStringとして保存するオプションを提供する
      • これは、新しいエンコーダに切り替えた場合でも、書き出されるバイト列が変化しないようにするために存在する
      • 既存の実装と互換性を維持するには、メジャーバージョンアップすると同時に、このオプションをONにする
    • また、ソースコードレベルの互換性を維持するため、String型とBinary型の両方をバイト列として返すモードを提供する

募集中のアイディア

「用語」で触れられている言語の種類を増やしたい。コメント求む。

この変更後のMessagePackの呼び方。

  • 案1:現行の仕様をMessagePack 0.9 と呼び、新しい仕様を MessagePack 1.0 と呼ぶ
  • 案2:現行の仕様をMessagePack 1.0 と呼び、新しい仕様を MessagePack 1.1 と呼ぶ
@methane
Copy link

methane commented Feb 23, 2013

あー、リテラルがあるだけで、それを判断できる型を使うかどうかは別の話ですね。

@kenn
Copy link

kenn commented Feb 23, 2013

様々な懸念点が払拭されていてStringの定義も簡潔になり、素晴らしいと思います!

バージョンに関しては、ライバルとなるJSONが http://www.json.org/fatfree.html

JSON has no version number. No revisions to the JSON grammar are anticipated. If something has a 1.0, it will inevitably get a 1.1 and a 2.0, and everything is crap until it is 3.0. JSON is very stable.

という哲学を表明しており、1.1だろうが2.0だろうがバージョンつけた時点で負け的な感じはあります。

今回の仕様がvery stableであると確信が持てるのであれば、、、微妙なところではありますが「バイナリフォーマットにおけるstringの扱い」という最難関を乗り越えた印象はあるので、今後の変更はもうないか、あっても今回以上に大きな変更はもう二度となさそうな気はするので、

  • 今回のものを便宜上「final」とし、バージョン番号はなし。100%の後方互換性と、使い方によっては(stringに確信犯的にバイナリを入れる運用をくずさないことで)100%の前方互換も達成可能。
    • 実装側では、むしろ後方互換モードをMessagePackCompatやMessagePackLegacyなどと名付ける?
    • 既存の実装者のなかでバージョンなしにこのような変更をすることに不満をおぼえる人もいるかもしれないが、長期的にはmsgpackが普及・成功してくれることを願っているはずなので、ぐっとこらえて協力してくれるはず、と信じたい!
  • やはりどうしてもバージョン番号がないと不便ということであれば、0.9 -> 1.0を推します
    • IETFなどに持っていくときには、バージョンなしが理想、あるいは1.0でいくのが今回の経緯を知らない人たちを巻き込む上ではもっとも不安をあたえず強力

@kazuho
Copy link

kazuho commented Feb 23, 2013

@kenn

実装側では、むしろ後方互換モードをMessagePackCompatやMessagePackLegacyなどと名付ける?

新しいものにソースコードレベルで MessagePack という名前を割り当ててしまうと、ソースコードレベルでの互換性を保ちにくいというのが問題になります。

注: Perlのモジュールの名前空間はグローバル、つまり、全モジュールが単一の名前空間にぶら下がります

現行の Perl 実装は以下のような感じです。

# hidekさんにhappy birthdayというメッセージを送る
my $mp = Data::MessagePack->new;
my $packed => $mp->pack({
    to             => 'hidek',
    message => 'happy birthday',
});

このコードのソース互換性を破壊せずに新フォーマットに対応する方法として Perl 界隈で「通常」とられる方法は以下のいずれかでしょう。

# バージョン番号をモジュール名に入れる (Data::MessagePack2 は Data::MessagePack と一体として配布可能)
my $mp = Data::MessagePack2->new;
# バージョン番号をコンストラクタの引数で指定する
my $mp = Data::MessagePack->new(VERSION => 2);

新旧2つのフォーマットが、いずれも "MessagePack" という名前で参照されるように Perl でできる唯一の方法は、"MessagePack" というモジュールが属する名前空間を変える方法です。以下の例は、"Data::MessagePack" という名前で現行方式のcodecを参照できるようにしつつ、"MessagePack" という新方式を実装したモジュール名をトップレベルに追加することで問題を回避しています。(名前空間がまたがっても新旧の両モジュールを一体として配布することはできます。また、両方のドキュメントでお互いに言及しておけば、「ドキュメントを読めばどっちがどっちを指してるのか分かる」というレベルにはなります)

my $mp = Data::MessagePack->new; # これは現行形式
my $mp = MessagePack->new; # これは新形式

もし、

やはりどうしてもバージョン番号がないと不便ということであれば、0.9 -> 1.0を推します

ということにする(つまり「今までのが development version だったんだよ!!!」と @frsyuki さんがいうことになる)のであれば、上のように名前空間を移動するのがベストな対応になるかとは思います。

だが、これは、MessagePack を知らない他のモジュールの開発者たちからは「驚き」をもって受け止められるでしょう。

@methane
Copy link

methane commented Feb 24, 2013

既存の実装との互換性のためには、 binary だけでなく string8 も使わない (従来の raw だけを使う) 必要がある事を
明記するべきだと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment