概要
Rubyを勉強した人ならば、「シンボルは内部では整数である」という話を一度は聞いたことがあると思います。そこで私は思いました。
- 「具体的に、どんな整数になっているんだろう?」
- 「逆に、Rubyのコード上で整数をシンボルとして使えないのはなぜだろう?」
これらについて解説した記事を探してみましたが、見つかりませんでした。そこで私は、RubyのC言語による実装を読んで自ら調べてみることにしました。この記事では、その過程でわかったことを紹介します。
注意
この記事で使用しているRubyのバージョンは3.4.3です。
前提
struct RSymbol {
struct RBasic basic;
st_index_t hashval;
VALUE fstr;
ID id;
};
Symbolは内部的にIDという値を持っています。また、シンボルは内部で2種類の配列に格納されており、それぞれ
- シンボルと文字列を紐づけるための配列(
str_sym
) - シンボルとシリアル番号(後述)を紐づけるための配列(
ids
)
として使われています。
調査
まずはsymbol.c
のintern_str
に、そのメソッドが呼ばれた時にシンボルとIDを表示するコードを追加します。
static ID
intern_str(VALUE str, int mutable)
{
ID id;
ID nid;
id = rb_str_symname_type(str, IDSET_ATTRSET_FOR_INTERN);
if (id == (ID)-1) id = ID_JUNK;
if (sym_check_asciionly(str, false)) {
if (!mutable) str = rb_str_dup(str);
rb_enc_associate(str, rb_usascii_encoding());
}
if ((nid = next_id_base()) == (ID)-1) {
str = rb_str_ellipsize(str, 20);
rb_raise(rb_eRuntimeError, "symbol table overflow (symbol %"PRIsVALUE")",
str);
}
id |= nid;
id |= ID_STATIC_SYM;
const char *cstr = StringValueCStr(str); // ここを追加
printf("intern_str called for %s with the id %ld\n", cstr, (long)id); // ここを追加
return register_static_symid_str(id, str);
}
これでmake
してRubyのシンボルを含む簡単なコードを実行してみると、出力結果はこうなります。
kasamina@(pc名):~/ruby-343/ruby-3.4.3$ ./ruby -e "p :foo; p :bar"
intern_str called for __autoload__ with the id 3985
intern_str called for dig with the id 4001
intern_str called for BasicObject with the id 4027
...(長いログ)...
intern_str called for foo with the id 44401
intern_str called for bar with the id 44417
intern_str called for $-p with the id 44439
intern_str called for $-l with the id 44455
intern_str called for $-a with the id 44471
:foo
:bar
シンボル:foo
に対してID44401
、:bar
に対してID44417
が発番されていることが分かります。
細かい発番機構を調べてみるために、さらにコードを追加します。
発番機構の調査
static ID
intern_str(VALUE str, int mutable)
{
ID id;
ID nid;
id = rb_str_symname_type(str, IDSET_ATTRSET_FOR_INTERN);
printf("id: %ld\n", (long)id); // ここを追加
if (id == (ID)-1) id = ID_JUNK;
if (sym_check_asciionly(str, false)) {
if (!mutable) str = rb_str_dup(str);
rb_enc_associate(str, rb_usascii_encoding());
}
if ((nid = next_id_base()) == (ID)-1) {
str = rb_str_ellipsize(str, 20);
rb_raise(rb_eRuntimeError, "symbol table overflow (symbol %"PRIsVALUE")",
str);
}
id |= nid;
id |= ID_STATIC_SYM;
printf("%ld, %ld\n", (long)nid, (long)ID_STATIC_SYM); // ここを追加
const char *cstr = StringValueCStr(str); // ここを追加
printf("intern_str called for %s with the id %ld\n", cstr, (long)id); // ここを追加
return register_static_symid_str(id, str);
}
これで./ruby -e "p :foo; p :bar"
を実行すると、最後の方の出力がこのように変わります。
id: 0
44400, 1
intern_str called for foo with the id 44401
id: 0
44416, 1
intern_str called for bar with the id 44417
id: 6
44432, 1
intern_str called for $-p with the id 44439
id: 6
44448, 1
intern_str called for $-l with the id 44455
id: 6
44464, 1
intern_str called for $-a with the id 44471
:foo
:bar
rb_str_symname_type
はシンボルの種別、nid
が本質的なIDで、ID_STATIC_SYM
は1で固定のようです。
そこで、nid
の定義を見てみましょう。
nid = next_id_base()
なので、next_id_base()
の定義を見に行きます。
static ID
next_id_base(void)
{
ID id;
GLOBAL_SYMBOLS_ENTER(symbols);
{
id = next_id_base_with_lock(symbols);
}
GLOBAL_SYMBOLS_LEAVE();
return id;
}
next_id_base
はnext_id_base_with_lock
を呼んでいます。symbols
はグローバルなシンボルのテーブルです。
symbols
の型であるrb_symbols_t
の定義はこれで、last_id
には「最後に発番したシリアル番号」が保存されています(id
という名前ですが保存するのはシリアル番号です)。
typedef struct {
rb_id_serial_t last_id;
st_table *str_sym;
VALUE ids;
VALUE dsymbol_fstr_hash;
} rb_symbols_t;
では実際にどんな番号が発番されるか調べるために、next_id_base_with_lock
の定義を見てみましょう。この関数はnext_id_base
のすぐ上にあります。
static ID
next_id_base_with_lock(rb_symbols_t *symbols)
{
ID id;
rb_id_serial_t next_serial = symbols->last_id + 1;
if (next_serial == 0) {
id = (ID)-1;
}
else {
const size_t num = ++symbols->last_id;
id = num << ID_SCOPE_SHIFT;
}
return id;
}
next_serial == 0
は特殊ケースなのでいったん考えないことにすると、symbol->last_id
に1を足してID_SCOPE_SHIFT
だけビットシフトさせたものがid
になることがわかります(symbol->last_id
はid
と付いていますが実際にはシリアル番号です)
ID_SCOPR_SHIFT
の値はid.h
で4と定義されているので、こうして16刻みのid
が得られます。
enum ruby_id_types {
(中略)
RUBY_ID_SCOPE_SHIFT = 4,
RUBY_ID_SCOPE_MASK = (~(~0U<<(RUBY_ID_SCOPE_SHIFT-1))<<1)
};
#define ID_STATIC_SYM RUBY_ID_STATIC_SYM
#define ID_SCOPE_SHIFT RUBY_ID_SCOPE_SHIFT
(後略)
その後
実はこのシリアル番号はRubyが内部で管理するシンボル一覧での通し番号であり、この後register_sym
とset_id_entry
によってシンボル一覧に登録されます。
Symbol
クラスの多くのメソッドは多くが引数としてID
型の値を取り、またRSymbol
も内部にはID
を持っているので、実際にはシリアル番号ではなくIDが「内部で扱われている整数」であることが多いようです。
他の場合
"foo".to_sym
のようにシンボルを作った場合は登録経路が異なります。
rb_str_intern
からdsymbol_alloc
に入り、register_sym
が呼ばれます。(私の環境では)SYMBOL_DEBUG
が0
なのでst_add_direct
に入り、内部のstr_sym
に登録されます。
一方、IDが発番されるのはもっと後で、IDが必要になってから初めてset_id_entry
を通してids
に記録されます。
ID
rb_sym2id(VALUE sym)
{
ID id;
if (STATIC_SYM_P(sym)) {
id = STATIC_SYM2ID(sym);
}
else if (DYNAMIC_SYM_P(sym)) {
GLOBAL_SYMBOLS_ENTER(symbols);
{
sym = dsymbol_check(symbols, sym);
id = RSYMBOL(sym)->id;
if (UNLIKELY(!(id & ~ID_SCOPE_MASK))) {
VALUE fstr = RSYMBOL(sym)->fstr;
ID num = next_id_base_with_lock(symbols);
RSYMBOL(sym)->id = id |= num;
/* make it permanent object */
set_id_entry(symbols, rb_id_to_serial(num), fstr, sym);
rb_hash_delete_entry(symbols->dsymbol_fstr_hash, fstr);
}
}
GLOBAL_SYMBOLS_LEAVE();
}
else {
rb_raise(rb_eTypeError, "wrong argument type %s (expected Symbol)",
rb_builtin_class_name(sym));
}
return id;
}
これ以外の場合はまだ調査していませんが、おそらく同じような経路にたどり着くのではないかと思います。
まとめ
-
Symbol
には2つの番号がある - シリアル番号は発行順に発番される
- IDはシリアル番号×16+種別(4bit)で計算される値
- 内部ではIDが使われることが多い