Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

CRubyコードリーディング(2) : rb_newobj_of

More than 5 years have passed since last update.

今回のテーマについて

筆者がCRuby実装を読んでみようという記事です。第二回です。

今回はsymbolの定義を行うrb_intern周りを読もうと思ったのですが、いくらか関数を掘ったところで
RString(文字列)に関連してfakeなRStringと、trueなRStringの使い分けがあり混乱しました。
その周辺コードを見るにRubyオブジェクトの確保後の初期化操作を理解していると読めそうです。
そこで新しいオブジェクトの確保を行っているらしいrb_newobj_ofを読むことで次につなげようと思います。

目的はオブジェクトの初期状態を知ることですから、
GCやアロケーションの詳しい話は保留する形で読んでいきます。

rb_newobj_of

rb_newobj_ofはgc.cに定義されています。

VALUE
rb_newobj_of(VALUE klass, VALUE flags)
{
    return newobj_of(klass, flags, 0, 0, 0);
}

どうやら実体はnewobj_ofのようです。
使っている側のコードを適当に探して読むと、klassにはクラスの種類を表現する定数、
flagsにはオブジェクトに関する各種のフラグをビットフラグ的な形で入れるようになっていました。

ところでそれぞれの引数や、戻り値の型はVALUEです。
ビットフラグ的に利用できるので自然数型であることは間違いないと思いますが、これはどんな型でしょうか?
そして呼出しの3つの引数の0はマジックナンバーですが、どんな意味があるのでしょうか。

それぞれ読んで調べてみました。

VALUE

VALUE型とは何でしょうか。

grepで調べると include/ruby/ruby.h の中にtypedefが見つかりました。

#if defined HAVE_UINTPTR_T && 0
typedef uintptr_t VALUE;
typedef uintptr_t ID;
# define SIGNED_VALUE intptr_t
# define SIZEOF_VALUE SIZEOF_UINTPTR_T
# undef PRI_VALUE_PREFIX
#elif SIZEOF_LONG == SIZEOF_VOIDP
typedef unsigned long VALUE;
typedef unsigned long ID;
# define SIGNED_VALUE long
# define SIZEOF_VALUE SIZEOF_LONG
# define PRI_VALUE_PREFIX "l"
#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP
typedef unsigned LONG_LONG VALUE;
typedef unsigned LONG_LONG ID;
# define SIGNED_VALUE LONG_LONG
# define LONG_LONG_VALUE 1
# define SIZEOF_VALUE SIZEOF_LONG_LONG
# define PRI_VALUE_PREFIX PRI_LL_PREFIX
#else
# error ---->> ruby requires sizeof(void*) == sizeof(long) or sizeof(LONG_LONG) to be compiled. <<----
#endif

その環境のポインタサイズに一致する何らかの型、のようですね。
単純にポインタや自然数型を使わず、VALUEをつかう意味は何でしょうか?

VALUEの用途をgrepで眺めると以下のことが何となくわかります。

  • 基本的にはRubyオブジェクトに対するポインタとして用いられている
  • ポインタではなく、自然数やビットフラグとして用いられている場合がある
    • 両者の区別は最下位ビットが0(ポインタ)であるかfixnum_flag(=1 / 自然数)であるかで行われる。
      • see also. include/ruby/ruby.h にある INT2FIX マクロ

恐らくメモリ効率化のための仕組みでしょう。
基本的にはポインタであるという知識が得られたので、十分です。
これ以上VALUEについて調査するのは今は避けることにしました。

newobj_of

rb_newobj_ofと同じくgc.cに定義があります。

static VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3)
{
   :
<<中略>>
   :
   :
    return obj;
}

rb_newobj_ofで指定されていたパラメタの0がv1~v3に入っているはずですね。

頭の方から読んでいきましょう。

static VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3)
{
    rb_objspace_t *objspace = &rb_objspace;
    VALUE obj;

    if (UNLIKELY(during_gc || ruby_gc_stressful)) {
    if (during_gc) {
        dont_gc = 1;
        during_gc = 0;
        rb_bug("object allocation during garbage collection phase");
    }

    if (ruby_gc_stressful) {
        if (!garbage_collect(objspace, FALSE, FALSE, FALSE, GPR_FLAG_NEWOBJ)) {
        rb_memerror();
        }
    }
    }
   :
<<後略>>

お。UNLIKELYですね。UNLIKELY指定されているので、
この条件節は基本的に起きないパスですから、一旦読み飛ばすことにしましょう。
(それに条件文を見る限りには、GC実行中の動作のようですし。)

変数宣言でobjspaceobjが確保されています。
rb_objspaceはgc.cの中でマクロが定義されていました。

#if defined(ENABLE_VM_OBJSPACE) && ENABLE_VM_OBJSPACE
#define rb_objspace (*GET_VM()->objspace)
#else
static rb_objspace_t rb_objspace = {{GC_MALLOC_LIMIT_MIN}};
#endif

見る限りは名前通り、オブジェクトを置くための領域のように思われます。
とりあえず目的は初期状態ですから、ここを深く掘る必要は無さそうですね。

newobj_of関数の中ほどに進みましょう。

static VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3)
{
<<前略>>
   :
    obj = heap_get_freeobj(objspace, heap_eden);

    if (RGENGC_CHECK_MODE > 0) assert(BUILTIN_TYPE(obj) == T_NONE);

    /* OBJSETUP */
    RBASIC(obj)->flags = flags & ~FL_WB_PROTECTED;
    RBASIC_SET_CLASS_RAW(obj, klass);
    if (rb_safe_level() >= 3) FL_SET((obj), FL_TAINT);
    RANY(obj)->as.values.v1 = v1;
    RANY(obj)->as.values.v2 = v2;
    RANY(obj)->as.values.v3 = v3;
<<後略>>
}

先に確保しておいたobjheap_get_freeobj関数でfreeobj(?)を獲得しているように読めます。
その後RBASICマクロにobjを通した結果に対してflagsを設定するようです。

FL_WB_PROTECTEDはgrepすると#define FL_WB_PROTECTED (((VALUE)1)<<5)
とあり、5ビット目をつかうビットフラグであるのが分かります。

この否定の論理積ですから、5ビット目だけはnewobj_offlags引数で指定されても無視するということですね。

FL_WB_PROTECTEDが何者かは気になりますが、初期値に関する知識としては
初期化時はその値が立たないことを知っていれば十分であると思います。
使う場所ではフラグ名がくるか5bit目への何らかの形でのアクセスがあるはずだからです。
(むしろこれから読んでいくことでFL_WB_PROTECTEDの意味も理解できるようになるでしょう。)

ということで気にしないことにします。

RBASICマクロは何でしょうか。ここではほかにもRANYが似たような用法で利用されています。
特にRANYでは引数のv1からv3を代入していますが、これはどんな効果があるのでしょうか。

またRBASIC_SET_CLASS_RAWFL_SETは何をしているのでしょうか。
そしてrb_safe_level()が3以上であるときはどんな時なのでしょうか。

  • ここの疑問点まとめ
    1. heap_get_freeobjは何をしているのか
    2. RBASICRANYはobjに対して何をしているのか。
      • 特にこの時、values.v1とかflagsとかでアクセスしているデータは何か?
    3. RBASIC_SET_CLASS_RAWは何をするマクロか
    4. rb_safe_level() >= 3とはどんな状況か

見ていきましょう。

1. heap_get_freeobjは何をしているのか

heap_get_freeobj()の定義は以下の通りです。

static inline VALUE
heap_get_freeobj(rb_objspace_t *objspace, rb_heap_t *heap)
{
    RVALUE *p = heap->freelist;

    while (1) {
    if (LIKELY(p != NULL)) {
        heap->freelist = p->as.free.next;
        return (VALUE)p;
    }
    else {
        p = heap_get_freeobj_from_next_freepage(objspace, heap);
    }
    }
}

あ、LIKELYですね。
LIKELYを手掛かりにすると、フリーリストから単純にオブジェクトを獲得するだけのようです。
目的に関係なさそうなのでメモリ確保できたということだけ理解して次に進みました。

heap_get_freeobjは単純にフリーリストからオブジェクトを確保している

2. RBASICRANYはobjに対して何をしているのか

RBASICRANYはobjに対して何をしているのかgrepしてみました。
結果としては以下の2つが見つかりました。

#define RBASIC(obj)  (R_CAST(RBasic)(obj))
#define RANY(o) ((RVALUE*)(o))

R_CASTについては以下の定義がありました

#define R_CAST(st)   (struct st*)

つまりどちらも、objをある構造体へのポインタとして見做すキャストを行っているということですね。
(RBASICマクロならRBasic構造体へのポインタ。RANYならRVALUE構造体へのポインタ。)

RBasic構造体とRVALUE構造体は、それぞれ以下のようなものでした。

struct RBasic {
    VALUE flags;
    const VALUE klass;
}
typedef struct RVALUE {
    union {
    struct {
        VALUE flags;        /* always 0 for freed obj */
        struct RVALUE *next;
    } free;
    struct RBasic  basic;
    struct RObject object;
    struct RClass  klass;
    struct RFloat  flonum;
    struct RString string;
    struct RArray  array;
    struct RRegexp regexp;
    struct RHash   hash;
    struct RData   data;
    struct RTypedData   typeddata;
    struct RStruct rstruct;
    struct RBignum bignum;
    struct RFile   file;
    struct RNode   node;
    struct RMatch  match;
    struct RRational rational;
    struct RComplex complex;
    struct RSymbol symbol;
    struct {
        struct RBasic basic;
        VALUE v1;
        VALUE v2;
        VALUE v3;
    } values;
    } as;
#if GC_DEBUG
    const char *file;
    int line;
#endif
} RVALUE;

GC_DEBUGはデバッグ情報だと思われるので無視しましょう。

RBasicはRVALUEの先頭部分。RVALUEはありとあらゆる組み込み型の共用体になっている様子です。
調べてみると、RVALUEに含まれる、それぞれの型の構造体定義の頭の部分が基本的にRBasicになっていました。
このことからRBasicはオブジェクト管理ヘッダとして共通に使われているらしいことが分かります。

RBasicにはflagsがあり、これはオブジェクト全体で共通のヘッダとしていますから、
元のコードでのflagsへの代入はこのヘッダへのフラグの代入だったということになります。

またRVALUEに含まれるそれぞれの型は小さく作られており、最大でポインタ3つ分(=VALUE3つ分)に収まっていました。

したがってRVALUEvaluesにあるv1からv3を経由して、
アクセスすることで、すべてのオブジェクトの値を適切に初期化することができそうです。

3. RBASIC_SET_CLASS_RAWは何をするマクロか

grepすると以下が見つかります。

#define RBASIC_SET_CLASS_RAW(obj, cls) (((struct RBasicRaw *)((VALUE)(obj)))->klass = (cls))

RBasicRaw構造体の定義もgrepすると

struct RBasicRaw {
    VALUE flags;
    VALUE klass;
};

このようになっていました。RBasicと非常に似ていますが、比べてみるとklassのconst性に違いがあるようです。
どうやらklass値に触るのは基本的に行うべきではなく、
必要なところではRBASIC_SET_CLASS_RAWを使って
操作するように保護をしているのではないかと思います。

ところでklassの値はクラス種別を示すらしき定数でした。
したがって結局RBASIC_SET_CLASS_RAWとは、指定のobjの型をclsとして記録するマクロと考えられます。

4. rb_safe_level() >= 3とはどんな状況か

最後にrb_safe_level()を読んでみましょう。
grepするとsafe.cの頭の方で見つかります。

int
rb_safe_level(void)
{
    return GET_THREAD()->safe_level;
}

そしてGET_THREADは、マクロかと思ったらinline関数で、

static inline rb_thread_t *
GET_THREAD(void)
{
    rb_thread_t *th = ruby_current_thread;
#if OPT_CALL_CFUNC_WITHOUT_FRAME
    if (UNLIKELY(th->passed_ci != 0)) {
    void vm_call_cfunc_push_frame(rb_thread_t *th);
    vm_call_cfunc_push_frame(th);
    }
#endif
    return th;
}

こんな感じ。カレントスレッドオブジェクトの取得かな?

で、ここまでしらべて「ああ。これってセキュリティモデルのセーフレベルじゃない?」と気付きました。
セキュリティモデル
スレッドごとにsafe_levelは持つでしょうし、直観的に噛みあいます。

ここで深く掘るのを辞めて、この理解で元のコードが解釈できるかを優先することにしました。

改めてnewobj_ofの中ほど

newobj_ofの中ほどを再掲します。

static VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3)
{
<<前略>>
   :
    obj = heap_get_freeobj(objspace, heap_eden);

    if (RGENGC_CHECK_MODE > 0) assert(BUILTIN_TYPE(obj) == T_NONE);

    /* OBJSETUP */
    RBASIC(obj)->flags = flags & ~FL_WB_PROTECTED;
    RBASIC_SET_CLASS_RAW(obj, klass);
    if (rb_safe_level() >= 3) FL_SET((obj), FL_TAINT);
    RANY(obj)->as.values.v1 = v1;
    RANY(obj)->as.values.v2 = v2;
    RANY(obj)->as.values.v3 = v3;
<<後略>>
}

FL_TAINTの意味がはっとしますね。

というところで、今までの理解で読んでみると、この部分は
1. オブジェクトを新たに確保して
2. オブジェクトのヘッダに指定されたflagsを設定して
3. オブジェクトのクラスを指定されたクラスに変更し
4. もしセキュリティレベルが3以上なら、オブジェクトに汚染状態フラグを立て
5. オブジェクトのボディ部(v1~v3)を指定された初期値に初期化する
- ここでrb_newobj_of経由の場合はv1~v3は0になります

と読めます。なるほど。初期化動作の理解としては十分な感触が得られた気がします。

そこで残りの部分に目を通すと、アサーションやデバッグ用のツール仕込みや、
ガーベジコレクタへの登録処理で、オブジェクトの初期状態を決定するような何かとは違っている様子でした。

他にやってることがないならオブジェクトの初期状態についての理解できたかなと判断しました。

結論

rb_newobj_ofで確保したオブジェクトの初期状態は以下のようなものです

  • オブジェクトのヘッダ部分は、flagsの指定からFL_WB_PROTECTEDを省いたフラグと、指定された通りのklassになります。
    • ただしセキュリティレベルが3以上の場合は、FL_TAINTも追加で立った状態になります。
  • オブジェクトのボディ部分は、0クリアされます

次回について

今回はかなり長くなってしまいました。次は簡単そうな部分を読んで短めで済ませたいですね。

その他の情報

この記事で読んでいるCRubyは、githubのruby/ruby、trunkブランチです。
現在のコミットハッシュ値は c1b05c53b795fdb1137819bc2973d591af2712d0 ですが、
常に最新版を今後も読む予定です。

shiracha
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away