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` 只是去套用該數值。