概要
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が使われることが多い