RubyのSymbolクラスの内部実装を覗いてみた

## 概要

Rubyを勉強した人ならば、「シンボルは内部では整数である」という話を一度は聞いたことがあると思います。そこで私は思いました。

「具体的に、どんな整数になっているんだろう?」

これについて解説した記事を探してみましたが、見つかりませんでした。そこで、RubyのC言語による実装を読んで自ら調べてみることにしました。この記事では、その過程でわかったことを紹介します。

## 注意

この記事で使用しているRubyのバージョンは3.4.3です。

## 前提


```symbol.h
struct RSymbol {
    struct RBasic basic;
    st_index_t hashval;
    VALUE fstr;
    ID id;
};
```

Symbolは内部的にIDという値を持っています。また、シンボルは内部で2種類の配列に格納されており、それぞれ

* シンボルと文字列を紐づけるための配列(`str_sym`)
* シンボルとシリアル番号(後述)を紐づけるための配列(`ids`)

として使われています。

## 調査

まずは`symbol.c`の`intern_str`に、そのメソッドが呼ばれた時にシンボルとIDを表示するコードを追加します。

```symbol.c
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のシンボルを含む簡単なコードを実行してみると、出力結果はこうなります。

```bash
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`に対してID`44401`、`:bar`に対してID`44417`が発番されていることが分かります。
細かい発番機構を調べてみるために、さらにコードを追加します。

### 発番機構の調査

```symbol.c
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()`の定義を見に行きます。

```symbols.c
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`という名前ですが保存するのはシリアル番号です)。

```symbols.h
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`のすぐ上にあります。

```symbol.c
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`が得られます。

```id.h
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`に記録されます。

```symbol.c
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が使われることが多い