Ruby向けのC拡張を書いている過程で、Rubiniusのバグらしきものを発見してしまいました。
FixnumとBignum
Rubyの整数は、メモリの続く限り大きな値を入れられるようになっています。ただ、整数演算が遅ければ言語としての使い勝手を大きく損なう事にもなります。
ということで、Rubyでは整数全般はInteger
という抽象クラスとして、実装は2つのクラスに分けています。
Fixnum
は速度を重視した、小さな整数についての型です。以前に触れましたが、Ruby内部では実際にオブジェクトを用意せず、object_id
と簡単に変換できるようになっています。Fixnumだと識別するためのビットが1ビット必要なので、32ビット環境では31ビット整数、64ビット環境では63ビット整数となっています1。
Fixnum
に入り切らないサイズの整数はBignum
になります。内部的には多倍長整数と言って、値を細かく分けて保存しているのですが、メソッドなどはFixnum
と共通なので、(速度が落ちることを除けば)Ruby上からは特に違いなく使えます。
ということで、Ruby上からは、よほどパフォーマンスにこだわるような場面を除いて、基本的にFixnum
とBignum
を識別する必要はまず生じません。
一方、C言語からRubyの拡張ライブラリを書く場合、Fixnum
はそのまま計算できるのに対して、Bignum
は関数やメソッド経由で使うことになるなど、まったくの別物となります。正しくハンドリングしなければ、うまく動きません。
Rubiniusで起きたこと
作成中のGemのテストをCRuby、JRuby、Rubiniusで行っていたところ、妙な現象が生じました。
Cライブラリに突入する関係上、Fixnum
とBignum
の違いが出てくる場面だったので、両者の境界値をテストケースに入れてテストを回していました。
すると、Rubiniusだけエラーで止まってしまいました。調べてみると、
-
-0x4000_0000_0000_0000
という、本来ならFixnum
となるはずの整数リテラルがBignum
となっていた - 負の数にビット反転をかけると、
0x3fff_ffff_ffff_ffff
となり、こちらはFixnum
になっていた -
Bignum
の符号反転がFixnum
になるという想定外の事態となり、TypeError
で死亡
という流れでした。C言語レベルの世界なら別として、Ruby側に「Fixnum
の範囲内に収まるBignum
」が出現しているのはよろしくないので、Rubinius側にIssueを立てています。
-
Windowsでは(x64でも)
long
が32ビットなので、Fixnumは31ビットしかありません。 ↩