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ビットしかありません。 ↩
