Slide 1

Slide 1 text

How to extend TracePoint TokyuRuby 会議13 @joker1007

Slide 2

Slide 2 text

またTracePoint か やあ (´ ・ω ・‵) ようこそ、プレモルハウスへ。 このプレモルはサービスだから、まず飲んで落ち着いて欲しい。 うん、「また」なんだ。済まない。 仏の顔もって⾔うしね、謝って許してもらおうとも思っていない。 でも、このLT を⾒たとき、君は、きっと⾔葉では⾔い表せない 「ときめき」みたいなものを感じてくれたと思う。 殺伐とした世の中で、そういう気持ちを忘れないで欲しい そう思って、このLT を⽴てたんだ。 じゃあ、注⽂を聞こうか。

Slide 3

Slide 3 text

今回はTracePoint ⾃体を拡張する話

Slide 4

Slide 4 text

TracePoint はメソッドの呼び出しとreturn をフックで きる そしてreturn は戻り値が取れる TracePoint#return_value

Slide 5

Slide 5 text

しかし、call された時の引数が取れない Ruby レベルのメソッドの場合は以下を駆使すれば取ろうと思えば取れる。 TracePoint#binding Binding#local_variable_get Method#parameters しかし、C で実装されたものはどうやっても取得できない。 C で実装されたメソッドはマッピングされたC の関数を呼んでるだけで、引数名も binding もない。

Slide 6

Slide 6 text

納得いかないので取れる様にしてみた

Slide 7

Slide 7 text

DEMO

Slide 8

Slide 8 text

時間切れまでどうやったかを話す

Slide 9

Slide 9 text

最初に -O0 でコンパイルしたRuby を⽤意する 簡単な機能だけでも gdb を使える様になっておく 正直gdb で⽌めて追っかけないと、相当詳しくない限りVM の処理を追うのは難しい。

Slide 10

Slide 10 text

そもそもTracePoint とは vm_trace.c に実装がある。 rb_tp_t という構造体が情報を保持している enable を呼ぶとvm ポインタを辿って global_event_hooks という箇所にhook 処 理を登録する 各イベントに対応した箇所に EXEC_EVENT_HOOK というマクロがあり、有効なhook 処理があればそこからhook が実⾏される

Slide 11

Slide 11 text

修正箇所の探し⽅ TracePoint の各イベントは RUBY_EVENT_ という形式で表現されている。 修正したい対象のevent でgrep すればすぐに⾒つかる。 今回のターゲットは RUBY_EVENT_C_CALL と RUBY_EVENT_CALL 。

Slide 12

Slide 12 text

:c_call の実⾏場所 vm_insnhelper.c の vm_call_cfunc_with_frame にある。 static VALUE vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc) { VALUE val; const rb_callable_method_entry_t *me = cc->me; const rb_method_cfunc_t *cfunc = vm_method_cfunc_entry(me); int len = cfunc->argc; VALUE recv = calling->recv; VALUE block_handler = calling->block_handler; int argc = calling->argc; RUBY_DTRACE_CMETHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, Qundef); vm_push_frame(ec, NULL, VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL, recv, block_handler, (VALUE)me, 0, ec->cfp->sp, 0, 0); if (len >= 0) rb_check_arity(argc, len, len); reg_cfp->sp -= argc + 1; val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); CHECK_CFP_CONSISTENCY("vm_call_cfunc"); rb_vm_pop_frame(ec); EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, recv, me->def->original_id, ci->mid, me->owner, val); RUBY_DTRACE_CMETHOD_RETURN_HOOK(ec, me->owner, me->def->original_id); return val; }

Slide 13

Slide 13 text

引数はどこか val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); 実際のC 関数を呼び出す処理はここ。 val は戻り値であり、 RUBY_EVENT_C_RETURN に付随データとして渡されている。 引数にあたるのは reg_cfp->sp + 1 。ここに引数情報がある。

Slide 14

Slide 14 text

sp って? 多分、スタックポインタの略。 RubyVM はスタックマシンであり、⼤体以下の様な仕組みで動作している。 スタックにオブジェクトを積む ISeq を取得する ISeq に対応した数だけスタックからオブジェクトをpop して命令を実⾏する 戻り値をスタックに積む これを延々繰り返す。 sp は VALUE のポインタであり、つまりRuby のスタックはオブジェクトとして表現可能 なものが連なった単なる連続したメモリ領域である。

Slide 15

Slide 15 text

Ruby におけるメソッド実⾏ 改めてISeq を確認する。 String.new("hoge") % ruby --dump=insns tokyu_experiment3.rb == disasm: #@tokyu_experiment3.rb:1 (1,0)-(1,18)> (catch: FALSE) 0000 opt_getinlinecache 7, ( 1)[Li] 0003 getconstant :String 0005 opt_setinlinecache 0007 putstring "hoge" 0009 opt_send_without_block , 0012 leave ISeq のsend 命令のバリエーションでメソッドが呼び出される。 opt_send_without_block の直前の putstring に注⽬。これが引数。 その上にある getconstant :String がレシーバ。

Slide 16

Slide 16 text

vm_call_cfunc_with_frame を再確認 val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); sp + 1 しているのはレシーバの位置にsp があるからであることが分かる。

Slide 17

Slide 17 text

TracePoint オブジェクトへのデータの受け渡し :c_return の return_value を参考にする。 EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, recv, me->def->original_id, ci->mid, me->owner, val); 最後の引数が return_value にあたる。

Slide 18

Slide 18 text

TracePoint#return_value の実装 VALUE rb_tracearg_return_value(rb_trace_arg_t *trace_arg) { if (trace_arg->event & (RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN | RUBY_EVENT_B_RETURN)) { /* ok */ } else { rb_raise(rb_eRuntimeError, "not supported by this event"); } if (trace_arg->data == Qundef) { rb_bug("rb_tracearg_return_value: unreachable"); } return trace_arg->data; } つまり EXEC_EVENT_HOOK の最後の引数を使えば rb_trace_arg_t の data に任意のオ ブジェクトを渡すことが出来る。

Slide 19

Slide 19 text

引数の取り⽅と、TracePoint への渡し⽅が分かった 後は配列を作って渡すだけ

Slide 20

Slide 20 text

最終的なパッチ diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 93b1ebfe7a..471395dc60 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -2200,7 +2200,13 @@ vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp int argc = calling->argc; RUBY_DTRACE_CMETHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, Qundef); + + VALUE argv = Qundef; + rb_hook_list_t *global_hooks = rb_vm_global_hooks(ec); + if (UNLIKELY(global_hooks->events & (RUBY_EVENT_C_CALL))) { + argv = rb_ary_new_from_values(argc, reg_cfp->sp - argc); + } + EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, argv); vm_push_frame(ec, NULL, VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL, recv, block_handler, (VALUE)me,

Slide 21

Slide 21 text

そして作ってから考えた

Slide 22

Slide 22 text

ユースケースが思い付かない! せっかく書いたんだけど、このままではパッチを送るだけの説得⼒が無い…… 。 というわけでユースケースを募集しております。 ( :call の⽅はsorbet の型定義を⾃動⽣成するとかに利⽤できるかもしれないけど。) ⽬的と⼿段が⼊れ替わるのはプログラミングではよくありますよね。

Slide 23

Slide 23 text

Appendix ( 時間⾜りないと思う)

Slide 24

Slide 24 text

:call イベントの場合 Ruby のメソッド定義は def と define_method で定義できる。 実はどちらで定義されたかによって呼び出しパスが異なる。 多分、 define_method だとスコープが切り替わらないからだと思う。

Slide 25

Slide 25 text

def の場合 trace_xxx というiseq の命令が vm_trace 関数を実⾏する。 最終的に vm_insnhelper.c の vm_trace_hook がhook を処理する。 static inline void vm_trace_hook(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *pc, rb_event_flag_t pc_events, rb_event_flag_t target_event, rb_hook_list_t *global_hooks, rb_hook_list_t *local_hooks, VALUE val) { rb_event_flag_t event = pc_events & target_event; VALUE self = GET_SELF(); VM_ASSERT(rb_popcount64((uint64_t)event) == 1); if (event & global_hooks->events) { /* increment PC because source line is calculated with PC-1 */ reg_cfp->pc++; vm_dtrace(event, ec); rb_exec_event_hook_orig(ec, global_hooks, event, self, 0, 0, 0 , val, 0); reg_cfp->pc--; } if (local_hooks != NULL) { if (event & local_hooks->events) { /* increment PC because source line is calculated with PC-1 */ reg_cfp->pc++; rb_exec_event_hook_orig(ec, local_hooks, event, self, 0, 0, 0 , val, 0); reg_cfp->pc--; } } }

Slide 26

Slide 26 text

env ポインタから引数を取る diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 93b1ebfe7a..471395dc60 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -4337,6 +4343,36 @@ vm_trace_hook(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VAL VM_ASSERT(rb_popcount64((uint64_t)event) == 1); + if (event & (RUBY_EVENT_CALL | RUBY_EVENT_B_CALL)) { + const rb_iseq_t *iseq = reg_cfp->iseq; + int local_table_size = iseq->body->local_table_size; + int not_keyword_arg_size = iseq->body->param.lead_num + iseq->body->param.opt_num + iseq->body->param.flags.has_rest + iseq->body->param.post_num; + + int keyword_size = 0; + int keyword_rest = 0; + if (iseq->body->param.keyword) { + keyword_size = iseq->body->param.keyword->num; + keyword_rest = iseq->body->param.keyword->rest_start; + } + + val = rb_ary_new_from_values(not_keyword_arg_size, reg_cfp->ep - (local_table_size + 2)); + + if (keyword_size > 0) { + const VALUE *keyword_args = reg_cfp->ep - (local_table_size + 2) + not_keyword_arg_size; + VALUE hash = rb_hash_new(); + int i; + for (i=0; ibody->param.keyword->table + i)), *(keyword_args + i)); + } + rb_ary_push(val, hash); + } + + if (keyword_rest > 0) { + const VALUE *keyword_rest = reg_cfp->ep - (local_table_size + 2) + not_keyword_arg_size + keyword_size + 1; + rb_ary_push(val, *keyword_rest); + } + } +

Slide 27

Slide 27 text

define_method の場合 vm.c の invoke_bmethod でhook を処理している。 static VALUE invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) { /* bmethod */ int arg_size = iseq->body->param.size; VALUE ret; rb_hook_list_t *hooks; VM_ASSERT(me->def->type == VM_METHOD_TYPE_BMETHOD); vm_push_frame(ec, iseq, type | VM_FRAME_FLAG_BMETHOD, self, VM_GUARDED_PREV_EP(captured->ep), (VALUE)me, iseq->body->iseq_encoded + opt_pc, ec->cfp->sp + arg_size, iseq->body->local_table_size - arg_size, iseq->body->stack_max); RUBY_DTRACE_METHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil); if (UNLIKELY((hooks = me->def->body.bmethod.hooks) != NULL) && hooks->events & RUBY_EVENT_CALL) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil, FALSE); } VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH); ret = vm_exec(ec, TRUE); EXEC_EVENT_HOOK(ec, RUBY_EVENT_RETURN, self, me->def->original_id, me->called_id, me->owner, ret); if ((hooks = me->def->body.bmethod.hooks) != NULL && hooks->events & RUBY_EVENT_RETURN) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_RETURN, self, me->def->original_id, me->called_id, me->owner, ret, FALSE); } RUBY_DTRACE_METHOD_RETURN_HOOK(ec, me->owner, me->def->original_id); return ret; }

Slide 28

Slide 28 text

sp からでも取れるが、直前の関数まで argv が渡ってきているので、直接渡せそう。 diff --git a/vm.c b/vm.c index 7ad6bdd264..436f0aa4c8 100644 --- a/vm.c +++ b/vm.c @@ -1031,7 +1031,7 @@ invoke_block(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, cons } static VALUE -invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) +invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, int argc, const VALUE *argv, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) { /* bmethod */ int arg_size = iseq->body->param.size; @@ -1049,12 +1049,18 @@ invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, co iseq->body->stack_max); RUBY_DTRACE_METHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil); + + VALUE data = Qundef; + rb_hook_list_t *global_hooks = rb_vm_global_hooks(ec); + if (UNLIKELY(global_hooks->events & (RUBY_EVENT_CALL))) { + data = rb_ary_new_from_values(argc, argv); + } + EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, data); if (UNLIKELY((hooks = me->def->body.bmethod.hooks) != NULL) && hooks->events & RUBY_EVENT_CALL) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_CALL, self, - me->def->original_id, me->called_id, me->owner, Qnil, FALSE); + me->def->original_id, me->called_id, me->owner, data, FALSE); } VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH); ret = vm_exec(ec, TRUE); @@ -1102,7 +1108,7 @@ invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_bl return invoke_block(ec, iseq, self, captured, cref, type, opt_pc); } else { - return invoke_bmethod(ec, iseq, self, captured, me, type, opt_pc); + return invoke_bmethod(ec, iseq, self, argc, argv, captured, me, type, opt_pc); } }