LoginSignup
7
7

More than 5 years have passed since last update.

[CRuby] Ruby VM 周辺(Kernel.#set_trace_func)2

Posted at

引き続き、Kernel.#set_trace_funcについて見ていきましょう。
目標は、イベントに対してフックする仕組みと、コールバックで呼び出す仕組みを理解することです。

CRubyからみたset_trace_func

使われているデータ構造

rb_event_flag_t

rb_event_flag_tはそのものずばり、イベントを区別するためのフラグのようです。

ruby.h
/* traditional set_trace_func events */
#define RUBY_EVENT_NONE      0x0000
#define RUBY_EVENT_LINE      0x0001
#define RUBY_EVENT_CLASS     0x0002
#define RUBY_EVENT_END       0x0004
#define RUBY_EVENT_CALL      0x0008
#define RUBY_EVENT_RETURN    0x0010
#define RUBY_EVENT_C_CALL    0x0020
#define RUBY_EVENT_C_RETURN  0x0040
#define RUBY_EVENT_RAISE     0x0080
#define RUBY_EVENT_ALL       0x00ff

/* for TracePoint extended events */
#define RUBY_EVENT_B_CALL          0x0100
#define RUBY_EVENT_B_RETURN        0x0200
#define RUBY_EVENT_THREAD_BEGIN    0x0400
#define RUBY_EVENT_THREAD_END      0x0800
#define RUBY_EVENT_TRACEPOINT_ALL  0xFFFF

/* special events */
#define RUBY_EVENT_SPECIFIED_LINE 0x10000
#define RUBY_EVENT_SWITCH         0x20000
#define RUBY_EVENT_COVERAGE       0x40000

typedef unsigned long rb_event_flag_t;

traditional set_trace_func events がline, call, return, c-call, c-return, class, end, raiseのイベントと対応しています(RUBY_EVENT_NONE,RUBY_EVENT_ALLという特殊なイベントもあるようですね)。

rb_event_hook_flag_t

次にrb_event_hook_flag_tです。これもフラグです(何の?)。

debug.h
typedef enum {
    RUBY_EVENT_HOOK_FLAG_SAFE    = 0x01,
    RUBY_EVENT_HOOK_FLAG_DELETED = 0x02,
    RUBY_EVENT_HOOK_FLAG_RAW_ARG = 0x04
} rb_event_hook_flag_t;

rb_event_hook_func_t

関数の関連でrb_event_hook_func_tです。
これこそが、set_trace_funcとイベントでやり取りされているコールバック関数(のためのポインタ)なのです(たぶん)。

ruby.h
typedef void (*rb_event_hook_func_t)(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass);

rb_event_hook_t

rb_event_hook_tです。

vm_trace.c
/* (1) trace mechanisms */

typedef struct rb_event_hook_struct {
    rb_event_hook_flag_t hook_flags; /* RUBY_EVENT_HOOK_FLAG_SAFEなど */
    rb_event_flag_t events; /* RUBY_EVENT_ALLなど */
    rb_event_hook_func_t func; /* procを呼び出すときに使う関数 */
    VALUE data; /* set_trace_func(proc)のproc */
    struct rb_event_hook_struct *next; /* 次の要素 */
} rb_event_hook_t;

trace mechanismsって書いてあります。nextから分かるようにリンクですね。
中身はイベントに関する2つのフラグと、コールバック関数、proc、次へのリンクです。

rb_hook_list_t

rb_hook_list_tです。
ChangeLogに説明があり

(1) add rb_hook_list_t data structure which includes
    hooks, events (flag) and `need_clean' flag.
    If the last flag is true, then clean the hooks list.
    In other words, deleted hooks are contained by `hooks'.
    Cleanup process should run before traversing the list.

何のことでしょう。。。

vm_core.h
typedef struct rb_hook_list_struct {
    struct rb_event_hook_struct *hooks;
    rb_event_flag_t events;
    int need_clean;
} rb_hook_list_t;

定義を見てみると、rb_event_hook_structがあり、ここがリンクのスタートになるようです。
ところでevent_flag_tがあるのですが、これはrb_event_hook_structにもあるので、どのような関係なのでしょうか。

イベントにコールバック関数を設定する

set_trace_funcの実装はvm_trace.cにあります。

Init_vm_traceで、Threadglobalに対してメソッドを定義しています。

vm_trace.c
/* This function is called from inits.c */
void
Init_vm_trace(void)
{
    /* trace_func */
    rb_define_global_function("set_trace_func", set_trace_func, 1);
    rb_define_method(rb_cThread, "set_trace_func", thread_set_trace_func_m, 1);
    rb_define_method(rb_cThread, "add_trace_func", thread_add_trace_func_m, 1);
...

もともとのset_trace_func

vm_trace.c
static VALUE
set_trace_func(VALUE obj, VALUE trace)
{
...
    rb_remove_event_hook(call_trace_func);
...
    if (!rb_obj_is_proc(trace)) {
    rb_raise(rb_eTypeError, "trace_func needs to be Proc");
    }

    rb_add_event_hook(call_trace_func, RUBY_EVENT_ALL, trace);
    return trace;
}

です。traceset_trace_func(proc)procです。RUBY_EVENT_ALLなので全てのイベントに対して設定されているようです。

call_trace_funcはさきほどみたrb_event_hook_func_tの実体になります。

typedef void (*rb_event_hook_func_t)(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass);
vm_trace.c
static void
call_trace_func(rb_event_flag_t event, VALUE proc, VALUE self, ID id, VALUE klass)
{
    const char *srcfile = rb_sourcefile();
    VALUE eventname = rb_str_new2(get_event_name(event));
    VALUE filename = srcfile ? rb_str_new2(srcfile) : Qnil;
    VALUE argv[6];
    int line = rb_sourceline();
    rb_thread_t *th = GET_THREAD();

    if (!klass) {
    rb_thread_method_id_and_class(th, &id, &klass);
    }

    if (klass) {
    if (RB_TYPE_P(klass, T_ICLASS)) {
        klass = RBASIC(klass)->klass;
    }
    else if (FL_TEST(klass, FL_SINGLETON)) {
        klass = rb_iv_get(klass, "__attached__");
    }
    }

    argv[0] = eventname;
    argv[1] = filename;
    argv[2] = INT2FIX(line);
    argv[3] = id ? ID2SYM(id) : Qnil;
    argv[4] = (self && srcfile) ? rb_binding_new() : Qnil;
    argv[5] = klass ? klass : Qnil;

    rb_proc_call_with_block(proc, 6, argv, Qnil);
}

argvにevent, file, line, id, binding, klassを詰め込んで、procをcallしていることから、これが最終的にprocを呼び出していると思います。

set_trace_funcからの呼び出しをまとめてみると

vm_trace.c
- static VALUE
  set_trace_func(VALUE obj, VALUE trace)

-- void
   rb_add_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data)

--- static rb_event_hook_t *
    alloc_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data,     rb_event_hook_flag_t hook_flags)

--- static void
    connect_event_hook(rb_hook_list_t *list, rb_event_hook_t *hook)

となります。

alloc_event_hookrb_event_hook_tの分のメモリを確保して、引数であるfunc, events, data, hook_flagsrb_event_hook_tのメンバにセットして返します。
connect_event_hookhooklist->hooksの先頭に挿入します。

vm_trace.c
void
rb_add_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data)
{
    rb_event_hook_t *hook = alloc_event_hook(func, events, data, RUBY_EVENT_HOOK_FLAG_SAFE);
    connect_event_hook(&GET_VM()->event_hooks, hook);
}

実装はこのようになっており、VMが管理しているevent_hooksに対して新しいhookを挿入しています。
GET_VM()

vm_core.h
#define GET_VM() ruby_current_vm

のことで、

vm.c
rb_vm_t *ruby_current_vm = 0;

となっています。ruby_current_vmの初期化はvm.cInit_VMInit_BareVMで行われています。
このあたりの処理はあとでみるとして、今は構造体をみてみましょう。

vm_core.h
typedef struct rb_vm_struct {
    VALUE self;

    rb_global_vm_lock_t gvl;
    rb_thread_lock_t    thread_destruct_lock;

    struct rb_thread_struct *main_thread;
    struct rb_thread_struct *running_thread;

    st_table *living_threads;
    VALUE thgroup_default;

    int running;
    int thread_abort_on_exception;
    int trace_running;
    volatile int sleeper;

    /* object management */
    VALUE mark_object_ary;

    VALUE special_exceptions[ruby_special_error_count];

    /* load */
    VALUE top_self;
    VALUE load_path;
    VALUE load_path_snapshot;
    VALUE load_path_check_cache;
    VALUE expanded_load_path;
    VALUE loaded_features;
    VALUE loaded_features_snapshot;
    struct st_table *loaded_features_index;
    struct st_table *loading_table;

    /* signal */
    struct {
    VALUE cmd;
    int safe;
    } trap_list[RUBY_NSIG];

    /* hook */
    rb_hook_list_t event_hooks;

    int src_encoding_index;

    VALUE verbose, debug, progname;
    VALUE coverages;

    struct unlinked_method_entry_list_entry *unlinked_method_entry_list;

#if defined(ENABLE_VM_OBJSPACE) && ENABLE_VM_OBJSPACE
    struct rb_objspace *objspace;
#endif

    /*
     * @shyouhei notes that this is not for storing normal Ruby
     * objects so do *NOT* mark this when you GC.
     */
    struct RArray at_exit;

    VALUE *defined_strings;

    /* params */
    struct { /* size in byte */
    size_t thread_vm_stack_size;
    size_t thread_machine_stack_size;
    size_t fiber_vm_stack_size;
    size_t fiber_machine_stack_size;
    } default_params;
} rb_vm_t;

このように

VM->event_hooks->hooks->next->next ...

rb_vm_struct->rb_hook_list_struct->rb_event_hook_struct->rb_event_hook_struct ...

という形でprocを管理しています。

7
7
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
7
7