Akata Works

東京エンジニア。主にRuby,Go,たまにAWSとiOS。ゲーム音楽が好きです。連絡はTwitterかakata.onen@gmail.comまで

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-8UTF-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")
""

うまく戻りましたね。やっぱり物事を理解するには深掘りするに限る。

参考