2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

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

  • 「具体的に、どんな整数になっているんだろう?」
  • 「逆に、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.cintern_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のシンボルを含む簡単なコードを実行してみると、出力結果はこうなります。

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

発番機構の調査

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_basenext_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_ididと付いていますが実際にはシリアル番号です)

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_symset_id_entryによってシンボル一覧に登録されます。

Symbolクラスの多くのメソッドは多くが引数としてID型の値を取り、またRSymbolも内部にはIDを持っているので、実際にはシリアル番号ではなくIDが「内部で扱われている整数」であることが多いようです。

他の場合

"foo".to_symのようにシンボルを作った場合は登録経路が異なります。

rb_str_internからdsymbol_allocに入り、register_symが呼ばれます。(私の環境では)SYMBOL_DEBUG0なので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が使われることが多い
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?