延續昨天的進度,當我們成功呼叫了 Block 之後會發現缺少了 OP_ENTER
(51
) 這個 OPCode 的實作,這個實作是用來將我們進入 Block 的時候把參數加入到現在正在使用的 Block 之中,在 mruby-L1VM 的實作基本上非常簡單,就是將資訊複製到當下的運行環境中,而 mruby 的實作就增加了非常多檢查,像是 Keyword Argument 的支援等等處理。
參考 mruby-L1VM 的實作發現再複製參數的時候是基於 parent
的 mrb_state
但是我們並沒有實作過,不過 parent
機制在我們的實作中要修改會影響到不少處理方式,因此我們需要製作一個 mrb_context
用來儲存目前呼叫的情境。
在之前實作方法的時候有提過
mrb_callinfo
就是屬於mrb_context
的資訊,這邊我們單純用mrb_context
來紀錄呼叫的資訊以及多層的處理。
先在 lib/iron/iron.h
加入新的結構資訊
// lib/iron/iron.h
typedef struct mrb_context {
mrb_value* stack;
} mrb_context;
typedef struct mrb_state {
int exc; /* exception */
int block;
struct kh_mt_s* mt;
mrb_callinfo* ci;
mrb_context* ctx;
} mrb_state;
因為只是要實現 Block 的呼叫,因此我們用最單純的方式在 mrb_state
中加入了 mrb_context
的指標,同時 mrb_context
會儲存當下呼叫的 stack
用於給後面其他 Block 的呼叫。
接下來只需要調整 lib/iron/vm.c
加入對應的處理:
// lib/iron/vm.c
// ...
CASE(OP_SENDB, BBB) {
const char* name = (const char*)irep_get(data, IREP_TYPE_SYMBOL, b);
khiter_t key = kh_get(mt, mrb->mt, name);
if (key == kh_end(mrb->mt)) {
DEBUG_LOG("func %s not found", name);
} else {
// ...
mrb_context ctx = { .stack = stack };
mrb->ctx = &ctx;
func(mrb);
}
NEXT;
}
// ...
首先是在 OP_SENDB
增加製做 mrb_context
的處理,並且把當下的 stack
儲存進去,再繼續呼叫。
接著我們實作 OP_ENTER
的處理,就能夠用最小的修改來實現這個功能。
// lib/iron/vm.c
// ...
CASE(OP_ENTER, W) {
int argc = a >> 18;
for(int i = 0; i < argc; i++) {
stack[i + 1] = mrb->ctx->stack[i + 1 + argc];
}
NEXT;
}
// ...
實際上在 OP_ENTER
需要做非常多的處理,不過為了能夠快速驗證 Block 是否能夠使用,我們只做了最少的處理。
除此之外,我們只做 a >> 18
的原因是 mruby 將呼叫資訊儲存在一個 32bit 的資訊中,我們需要透過向右移動 18bit 排除掉多餘的資訊才能拿到正確的參數數量,而其他的資訊目前都還用不到因此就跳過不處理,在 mruby 的原始碼會發現 OP_ENTER
要做的事情實際上是非常多的。
當我們完成 OP_ENTER
之後,會發現出現要我們實作 OP_GETUPVAR
(31
) 和 OP_SETUPVAR
(32
) 的處理,這是因為我們在實作中 Block 參考了外部的 i
並且需要更新計數的關係,跟 OP_ENTER
的實作上類似,我們只需要透過 mrb_context
指向的「上層」寄存器資料,就可以直接更新,而 mruby 的編譯器也都已經幫我們設定好正確的數值。
// lib/iron/vm.c
// ...
CASE(OP_GETUPVAR, BBB) goto L_UPVAR;
CASE(OP_SETUPVAR, BBB) {
L_UPVAR:
// Move to specify parent via "c"
if (insn == OP_GETUPVAR) {
DEBUG_LOG("r[%d] = r[%d] of up:%d", a, b, c);
stack[a] = mrb->ctx->stack[b];
} else {
DEBUG_LOG("r[%d] of up:%d = r[%d]", b, c, a);
mrb->ctx->stack[b] = stack[a];
}
NEXT;
}
// ...
有了這些實作之後,我們就可以正確的呼叫 Block 執行裡面的邏輯。不過這樣的設計實際上有很大的限制,因為我們的 mrb_context
只能處理「單層」但是參考的變數很多時候可能是多層的,這也是為什麼還會有一個 c
(往外幾層)的資訊需要被實作。
到了這個階段,似乎也能夠理解像是全域變數之類的為什麼會拖慢運行速度,假設 VM 在設計上沒有特別優化的話,每次執行都需要不斷往外層尋找才能處理的狀況下就會增加非常多不必要的處理。
雖然我們可以呼叫 Block 了,但是實際上只會運作一次就停止,因此我們需要實際套用迴圈到 mrb_loop
的實作中讓他能夠不斷的運行直到 Block 呼叫 return
或者 break
為止。
// src/hal_arduino/embed_methods.c
// ...
void mrb_loop(mrb_state* mrb) {
int argc = mrb_get_argc(mrb);
mrb_value* argv = mrb_get_argv(mrb);
mrb_value blk = argv[argc - 1];
do {
mrb->ctx->block = 1;
mrb_exec(mrb, (const uint8_t*)blk.value.p);
} while(mrb->ctx->block);
}
// ...
這次我們對 mrb_context
加入了一個 block
判斷資訊,用來區分是否還是處於 Block 的處理中,如果離開的話就會將它設定為 0
此時在 C 語言實作的迴圈就會中斷,而我們就可以順利的終止無限迴圈。
我們先在 OP_SENDB
調整了 mrb_context
的生成加入了預設的 block = 1
作為初始值。
// lib/iron/vm.c
// ...
mrb_context ctx = { .stack = stack, .block = 1 };
mrb->ctx = &ctx;
// ...
然後對 OP_RETURN
和 OP_BREAK
加入 block = 0
的設置讓我們遭遇到 return
或者 break
的處理時能夠終止迴圈。
// lib/iron/vm.c
// ...
CASE(OP_BREAK, B) goto L_RETURN;
CASE(OP_RETURN, B) {
L_RETURN:
DEBUG_LOG("%s r[%d]", insn == OP_RETURN ? "return" : "break", a);
if (mrb->ctx && insn == OP_BREAK) {
mrb->ctx->block = 0;
}
return mrb_fixnum(stack[a]);
}
// ...
不過這邊的實作我們還有一點小問題,那就是每次 Block 呼叫完畢後都會執行一次 return
的處理,也因此我們如果在 OP_RETURN
的時候將 mrb->ctx->block
設定為 0
就會讓迴圈無法繼續運行,進而造成我們無法直接在 Block 裡面直接呼叫 return
的處理。
不過即使在 CRuby 中呼叫
return
也是有條件限制的,不然會出現像是 LocalJump 的錯誤。
下一篇我們會來討論還沒實作的回傳值處理,目前 mrb_exec
回傳的還是一個整數值,根據 Block 的行為來看他應該要是 mrb_value
並且能夠傳遞給其他人接續使用。