search
LoginSignup
7
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

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

引き続き、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を管理しています。

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
What you can do with signing up
7
Help us understand the problem. What are the problem?