RubyのCSI多言語化について調べたらpack, unpackについて理解できた話
Rubyでバイト列の先頭にマジックナンバーが付いたようなものを扱う機会があったのでArray#pack, String#unpackについて調べてみました。
このあたりの認識がふわっとしてると、使うときに障害の種になるので要注意ですね。
結構調べたつもりですが、間違えていたらスミマセン。
Rubyの多言語化手法はCSI方式
まず第一に、Rubyの多言語化手法がCSI方式であるという点について知っておかないと、pack, unpackがふわっとします(僕はしました)
多くの言語では、文字集合と文字コードを統一するUCS方式という多言語化手法が使われており、例えばPerlだと文字集合をUnicode、文字コードをUTF-8として統一しています。なので、外から入ってくるバイト列は必ずUTF-8にエンコードし、外に出すときにまたエンコードするといった感じです。
対してRubyのようなCSI方式では、文字列オブジェクト自体が自らの文字コード情報を持っています。つまり、文字列オブジェクトが持つ主な情報はバイト列と文字コードです。
> puts "あ".encoding UTF-8 > puts "あ".bytes 227 # 0xE3 129 # 0x81 130 # 0x82
Ruby 2.0から標準内部エンコーディングがUTF-8なので文字コードは標準ではUTF-8に、UTF-8における"あ"は 16進数ではE3 81 82なので上記のような結果になります。
CSI方式では言語仕様の文字集合や文字コードに依存しないなど、それぞれの方式には得手不得手があるのですが、ここでは省略します。詳しくは => Ruby M17N の設計と実装
String#unpack
本題になりますが、String#unpackについてで、これは文字列オブジェクトが持つバイト列情報をテンプレート文字列(第一引数)に従って変換するメソッドです。出力結果は配列で返ってきますが、テンプレート文字列によっては一要素しか含みません。
テンプレート文字列は多いので端折ります => pack テンプレート文字列 (Ruby 2.5.0)
> puts "あ".unpack("H*") # H: 上位ニブルが先の16進数表現 e38182 > puts "あ".unpack("C*") # H: 8bit符号なし整数表現 227 # 0xE3 129 # 0x81 130 # 0x82
UTF-8における"あ"は E3 81 82でしたね。
Array#pack
続いて、Array#packについてです。これは逆にString#unpackメソッドとテンプレート文字列で表現可能な配列を文字列オブジェクトに戻せるメソッドです。
本来、packの説明をベースにunpackはそれを戻すものみたいに言われてますが、個人的には文字列のほうが直感的かと思ったので、先にunpackを説明して、packはそれを戻すもとと解釈したほうが分かりやすかったです。
> p ["e38182"].pack("H*") "\xE3\x81\x82" > p [227, 129, 130].pack("C*") "\xE3\x81\x82"
ここで「話が違うぞ!!」と慌てる前に、まずは文字コードを確認するといいです。
> puts ["e38182"].pack("H*").encoding ASCII-8BIT > puts [227, 129, 130].pack("C*").encoding ASCII-8BIT
Ruby 2.0以降の標準文字コードはUTF-8でしたが、packの返却値はどうやらASCII-8BITみたいですね。同じバイト列でも、文字コードが違うだけで見え方が変わっています。
ちなみに、IOオブジェクトの標準外部エンコーディングはいい感じに設定されているので、上のコードはあえてpで出力してますが、普通にputsで出力すると、自分でUTF-8に戻さなくても出力結果はUTF-8(環境によりけり)になります。
> p ["e38182"].pack("H*").force_encoding("UTF-8") "あ" > p [227, 129, 130].pack("C*").force_encoding("UTF-8") "あ"
うまく戻りましたね。やっぱり物事を理解するには深掘りするに限る。