2020 年 10 月 === # 蒼時 ## 翻找原始碼 這塊其實不太容易,需要先在原始碼把跟 `super` 相關的檔案找出來,但裡面混雜不少東西 > CRuby 2.6 的 `super` 是關鍵字,可以從這個方向下手 ## Bison 基本上 Ruby 體系的語言都是用 Yacc 的格式處理,這塊沒學過不是那麼容易閱讀,大致上來說我們可以在 `parse.y` 裡面找到關於 `super` 相關的幾段程式碼。 > 這塊我自己也不算熟悉 根據 `parse.y` 裡面產生的程式碼往下追,會在 `node.h` 找到定義,這是用來描述 AST 結構的行為。 ```c // node.h #define NEW_SUPER(a,loc) NEW_NODE(NODE_SUPER,0,0,a,loc) #define NEW_ZSUPER(loc) NEW_NODE(NODE_ZSUPER,0,0,0,loc) ``` > Ruby 的巨集其實寫的很直覺,大致上可以看出來目的是製作 AST 的 Node ## Compiler / AST 把目標先鎖定在 `NODE_SUPER` 的處理,可以在 `compile.c` 和 `ast.c` 找到對應的處理。 ```c // ast.c case NODE_SUPER: return rb_ary_new_from_node_args(ast, 1, node->nd_args); case NODE_ZSUPER: return rb_ary_new_from_node_args(ast, 0); ``` 這塊蠻有趣的,在 AST 裡面產生的是 Ruby 的 Array,不過他是 `from_node_args` 也可能跟 Ruby 的 Array 沒有關係,不過考慮到 Ruby 2.6+ 開始把 VM 的一些資訊開放 API 讓 Ruby 可以存取到,在建構 AST 的時候就製作成 Ruby Array 是蠻有可能的。 ```c // compile.c case NODE_SUPER: case NODE_ZSUPER: ADD_INSN(ret, line, putnil); ADD_INSN3(ret, line, defined, INT2FIX(DEFINED_ZSUPER), 0, needstr); return; ``` 這塊看起來就蠻直觀的,這個 `ADD_INSN` 巨集應該是用來製作 OP Code 的處理,能找到對應的 OP Code 就可以去反推 Ruby VM 的處理。 然後除了第一段之外還有第二段,長得有點懷疑人生⋯⋯ ```c // compile.c case NODE_SUPER: case NODE_ZSUPER:{ DECL_ANCHOR(args); int argc; unsigned int flag = 0; struct rb_call_info_kw_arg *keywords = NULL; const rb_iseq_t *parent_block = ISEQ_COMPILE_DATA(iseq)->current_block; INIT_ANCHOR(args); ISEQ_COMPILE_DATA(iseq)->current_block = NULL; if (type == NODE_SUPER) { VALUE vargc = setup_args(iseq, args, node->nd_args, &flag, &keywords); CHECK(!NIL_P(vargc)); argc = FIX2INT(vargc); } else { /* NODE_ZSUPER */ int i; const rb_iseq_t *liseq = body->local_iseq; const struct rb_iseq_constant_body *const local_body = liseq->body; const struct rb_iseq_param_keyword *const local_kwd = local_body->param.keyword; int lvar_level = get_lvar_level(iseq); argc = local_body->param.lead_num; /* normal arguments */ for (i = 0; i < local_body->param.lead_num; i++) { int idx = local_body->local_table_size - i; ADD_GETLOCAL(args, line, idx, lvar_level); } if (local_body->param.flags.has_opt) { /* optional arguments */ int j; for (j = 0; j < local_body->param.opt_num; j++) { int idx = local_body->local_table_size - (i + j); ADD_GETLOCAL(args, line, idx, lvar_level); } i += j; argc = i; } if (local_body->param.flags.has_rest) { /* rest argument */ int idx = local_body->local_table_size - local_body->param.rest_start; ADD_GETLOCAL(args, line, idx, lvar_level); ADD_INSN1(args, line, splatarray, Qfalse); argc = local_body->param.rest_start + 1; flag |= VM_CALL_ARGS_SPLAT; } if (local_body->param.flags.has_post) { /* post arguments */ int post_len = local_body->param.post_num; int post_start = local_body->param.post_start; if (local_body->param.flags.has_rest) { int j; for (j=0; j<post_len; j++) { int idx = local_body->local_table_size - (post_start + j); ADD_GETLOCAL(args, line, idx, lvar_level); } ADD_INSN1(args, line, newarray, INT2FIX(j)); ADD_INSN (args, line, concatarray); /* argc is settled at above */ } else { int j; for (j=0; j<post_len; j++) { int idx = local_body->local_table_size - (post_start + j); ADD_GETLOCAL(args, line, idx, lvar_level); } argc = post_len + post_start; } } if (local_body->param.flags.has_kw) { /* TODO: support keywords */ int local_size = local_body->local_table_size; argc++; ADD_INSN1(args, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE)); if (local_body->param.flags.has_kwrest) { int idx = local_body->local_table_size - local_kwd->rest_start; ADD_GETLOCAL(args, line, idx, lvar_level); ADD_SEND (args, line, rb_intern("dup"), INT2FIX(0)); } else { ADD_INSN1(args, line, newhash, INT2FIX(0)); } for (i = 0; i < local_kwd->num; ++i) { ID id = local_kwd->table[i]; int idx = local_size - get_local_var_idx(liseq, id); ADD_INSN1(args, line, putobject, ID2SYM(id)); ADD_GETLOCAL(args, line, idx, lvar_level); } ADD_SEND(args, line, id_core_hash_merge_ptr, INT2FIX(i * 2 + 1)); if (local_body->param.flags.has_rest) { ADD_INSN1(args, line, newarray, INT2FIX(1)); ADD_INSN (args, line, concatarray); --argc; } flag |= VM_CALL_KW_SPLAT; } else if (local_body->param.flags.has_kwrest) { int idx = local_body->local_table_size - local_kwd->rest_start; ADD_GETLOCAL(args, line, idx, lvar_level); ADD_SEND (args, line, rb_intern("dup"), INT2FIX(0)); if (local_body->param.flags.has_rest) { ADD_INSN1(args, line, newarray, INT2FIX(1)); ADD_INSN (args, line, concatarray); } else { argc++; } flag |= VM_CALL_KW_SPLAT; } } ADD_INSN(ret, line, putself); ADD_SEQ(ret, args); ADD_INSN2(ret, line, invokesuper, new_callinfo(iseq, 0, argc, flag | VM_CALL_SUPER | (type == NODE_ZSUPER ? VM_CALL_ZSUPER : 0) | VM_CALL_FCALL, keywords, parent_block != NULL), parent_block); if (popped) { ADD_INSN(ret, line, pop); } break; } ``` 大致是看起來像是在處理 `super(...)` 的情況,不過我們可以先跳過細節,這邊關鍵應該是 `ret` 是什麼,並且 `ADD_INSN` 如何去判斷 OP Code 是什麼。 ```c /* add an instruction */ #define ADD_INSN(seq, line, insn) \ ADD_ELEM((seq), (LINK_ELEMENT *) new_insn_body(iseq, (line), BIN(insn), 0)) ``` 問題應該會是 `LINK_ELEMENT` 或者在前面這幾段 `ret` 對應的 `LINK_ANCHOR` 是什麼東西為關鍵,所以需要繼續往下追。 ```c typedef struct iseq_link_element { enum { ISEQ_ELEMENT_ANCHOR, ISEQ_ELEMENT_LABEL, ISEQ_ELEMENT_INSN, ISEQ_ELEMENT_ADJUST, ISEQ_ELEMENT_TRACE, } type; struct iseq_link_element *next; struct iseq_link_element *prev; } LINK_ELEMENT; typedef struct iseq_link_anchor { LINK_ELEMENT anchor; LINK_ELEMENT *last; } LINK_ANCHOR; ``` `LINK_ANCHOR` 和 `LINK_ELEMENT` 看起來都是 AST 的一部分,這樣問題又回到 OP Code 是在哪裡被定義的,才能回推 Ruby VM 的實際行為。 回去看 `ADD_INSN` 行為,第三個參數是指令碼,在 `insns.def` 可以找到 ```c /* put nil to stack. */ DEFINE_INSN putnil () () (VALUE val) { val = Qnil; } ``` 這邊大致上可以確定前面看到的處理的 OP Code 要從哪裡找,仔細看比較大串的地方會找到 ```c ADD_SEQ(ret, args); ADD_INSN2(ret, line, invokesuper, new_callinfo(iseq, 0, argc, flag | VM_CALL_SUPER | (type == NODE_ZSUPER ? VM_CALL_ZSUPER : 0) | VM_CALL_FCALL, keywords, parent_block != NULL), parent_block); ``` 這邊放入的指令碼 `invokesuper` 應該就是實際呼叫 `super` 處置,後面的 `new_callinfo` 則是 `super` 呼叫傳入的參數,因此我們在 Ruby VM 實作去找 `invokesuper` 對應的行為就能反推相關的處理。 ## Ruby VM 尋找了 `invokesuper` 會發現並沒有在 Ruby VM 裡面實作,而是在 `compile.c` 裡面還有優化的處理。 ```c // compile.c if (do_tailcallopt && (IS_INSN_ID(iobj, send) || IS_INSN_ID(iobj, opt_aref_with) || IS_INSN_ID(iobj, opt_aset_with) || IS_INSN_ID(iobj, invokesuper))) { /* * send ... * leave * => * send ..., ... | VM_CALL_TAILCALL, ... * leave # unreachable */ INSN *piobj = NULL; if (iobj->link.next) { LINK_ELEMENT *next = iobj->link.next; do { if (!IS_INSN(next)) { next = next->next; continue; } switch (INSN_OF(next)) { case BIN(nop): next = next->next; break; case BIN(jump): /* if cond * return tailcall * end */ next = get_destination_insn((INSN *)next); break; case BIN(leave): piobj = iobj; /* fall through */ default: next = NULL; break; } } while (next); } if (piobj) { struct rb_call_info *ci = (struct rb_call_info *)OPERAND_AT(piobj, 0); if (IS_INSN_ID(piobj, send) || IS_INSN_ID(piobj, invokesuper)) { if (OPERAND_AT(piobj, 1) == 0) { /* no blockiseq */ ci->flag |= VM_CALL_TAILCALL; } } else { ci->flag |= VM_CALL_TAILCALL; } } } ``` 像是上面這段是 Tail Call 的優化處理。 回到 `insns.def` 找線索,因為除了 `compile.c` 之外沒有其他資訊。 ```c /* super(args) # args.size => num */ DEFINE_INSN invokesuper (CALL_DATA cd, ISEQ blockiseq) (...) (VALUE val) // attr rb_snum_t sp_inc = sp_inc_of_sendish(&cd->ci); // attr rb_snum_t comptime_sp_inc = sp_inc_of_sendish(ci); { VALUE bh = vm_caller_setup_arg_block(ec, GET_CFP(), &cd->ci, blockiseq, true); val = vm_sendish(ec, GET_CFP(), cd, bh, vm_search_super_method); if (val == Qundef) { RESTORE_REGS(); NEXT_INSN(); } } ``` 看起來 CRuby 會直接在 `insns.def` 描述實作的內容,實際上的 `super` 處理其實就在上面這段程式碼中。 * 呼叫 `vm_caller_setup_arg_block` 方法 * 呼叫 `vm_sendish` 方法,推測 `val` 會是呼叫後的回傳值 目前問題是 `insns.def` 產生的檔案為何,不過要判斷 `super` 的行為應該是足夠。 ## `vm_search_super_method` 上面的 `vm_sendish` 裡面呼叫了 `vm_search_super_method` 應該就會是 `super` 行為的本體。 ```c static void vm_search_super_method(const rb_control_frame_t *reg_cfp, struct rb_call_data *cd, VALUE recv) { VALUE current_defined_class, klass; const rb_callable_method_entry_t *me = rb_vm_frame_method_entry(reg_cfp); struct rb_call_info *ci = &cd->ci; struct rb_call_cache *cc = &cd->cc; if (!me) { vm_super_outside(); } current_defined_class = me->defined_class; if (!NIL_P(RCLASS_REFINED_CLASS(current_defined_class))) { current_defined_class = RCLASS_REFINED_CLASS(current_defined_class); } if (BUILTIN_TYPE(current_defined_class) != T_MODULE && BUILTIN_TYPE(current_defined_class) != T_ICLASS && /* bound UnboundMethod */ !FL_TEST(current_defined_class, RMODULE_INCLUDED_INTO_REFINEMENT) && !rb_obj_is_kind_of(recv, current_defined_class)) { VALUE m = RB_TYPE_P(current_defined_class, T_ICLASS) ? RBASIC(current_defined_class)->klass : current_defined_class; rb_raise(rb_eTypeError, "self has wrong type to call super in this context: " "%"PRIsVALUE" (expected %"PRIsVALUE")", rb_obj_class(recv), m); } if (me->def->type == VM_METHOD_TYPE_BMETHOD && (ci->flag & VM_CALL_ZSUPER)) { rb_raise(rb_eRuntimeError, "implicit argument passing of super from method defined" " by define_method() is not supported." " Specify all arguments explicitly."); } ci->mid = me->def->original_id; klass = vm_search_normal_superclass(me->defined_class); if (!klass) { /* bound instance method of module */ cc->aux.method_missing_reason = MISSING_SUPER; CC_SET_FASTPATH(cc, vm_call_method_missing, TRUE); } else { /* TODO: use inline cache */ cc->me = rb_callable_method_entry(klass, ci->mid); CC_SET_FASTPATH(cc, vm_call_super_method, TRUE); } } ``` 行為上大致上看起來是這樣 * 製作呼叫相關結構 * 檢查 `me` (Method Entry) * 如果不存在表示不是在 Method 中呼叫,拋出錯誤 * 檢查目前的 Class 有沒有被 `refine` 修改過 * 檢查 Class 是否正確 Ex. 呼叫者是否是該 Class 實體、是否為 Module 等 * 檢查 `BMETHOD` 情境(跟 `ZSUPER` 處理有關,推測是無法自動推出 `super` 參數的情況) * 搜尋 `superclass` 的方法 * 設定 `mid` (Method ID) 作為呼叫用的參考 > 這塊沒有看到怎麼持續往上找的處理,有機會可以再繼續往下找 ``` MJIT_STATIC const rb_callable_method_entry_t * rb_vm_frame_method_entry(const rb_control_frame_t *cfp) { const VALUE *ep = cfp->ep; rb_callable_method_entry_t *me; while (!VM_ENV_LOCAL_P(ep)) { if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) return me; ep = VM_ENV_PREV_EP(ep); } return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE); } ``` 推測是在製作相關結構(Method Entry)的時候就已經跑過迴圈往回找到上一層定義的 Method Entry 來當作參考,所以實際呼叫 Super 最後設定 `mid` 只是去套用該數值。