之前我們有提到過 loop
方法因為我們並沒有實作 Block 機制而無法使用,而 Block 和 Method 實際上是在 Ruby 的 IREP 中製作出一個子 IREP 來紀錄某個 Block 和方法的行為,實際上在 CRuby 裡面我們可以利用 #method
方法把某個物件的方法轉換為 Method
物件,而這個物件又能夠透過 to_proc
轉換為 Proc
物件。
上面這一連串的處理,實際上就是 Ruby 在語言層級上對方法和 Block 的理解,從宏觀的角度來看不論是方法、Proc 或者是 Lambda 和 Block 實際上都是 Proc 的一種表現,不過在 CRuby 中的 Method 跟 Proc 還是有一些差異的,而在 mruby 和 mruby-L1VM 裡面則是都當作類似 Proc 的方式處理。
我們先修改 app.rb
製作出一個需要產生 Block
的情況
i = 0
loop do
i += 1
break if i >= 5
end
當我們運行 pio test
的時候會得到 OP_BLOCK
(85
) 找不到的錯誤,跟之前所有的實作一樣先在 lib/iron/opcode.h
加入對應的指令
// lib/iron/opcode.h
// ...
OP_BLOCK = 85,
OP_METHOD,
// ...
接著到 lib/iron/vm.c
實作對應的行為
// lib/iron/vm.c
// ...
CASE(OP_BLOCK, BB) {
mrb_value proc;
proc.tt = MRB_TT_PROC;
proc.value.p = (void*)irep_get(data, IREP_TYPE_IREP, b);
stack[a] = proc;
DEBUG_LOG("r[%d] = lamda(irep[%d],%s)", a, b, insn == OP_BLOCK ? "L_BLOCK" : "L_METHOD");
NEXT;
}
// ...
實作上看起來比想像中的簡單,我們只需要製作一個 mrb_value
變數,然後將 irep_get
取出的子 IREP 區段儲存到指標裡面,然後再將他儲存到指定的寄存器裡面就完成了處理。
唯一不同的地方是我們還需要去 lib/iron/value.h
增加 MRB_TT_PROC
這個物件類型,用來區分我們現在傳遞的是一個 Block 物件。
在 CRuby 裡面還會有
RProc*
的結構用來處理相關的資訊,同時也會註明是否為lambda
類型,所以大致上來說在 Ruby 需要區分的是lambda
和 Proc 的差異,以及能不能被轉換成 Proc。
當我們時做了 OP_BLOCK
指令後,就會發現馬上進入呼叫方法的環節,跟之前呼叫的 OP_SEND
不同的地方是這次我們呼叫的是 OP_SENDB
指令,在這邊蟲魚理解到這個 B
是表示 Block 的意思,因此我們除了確認 lib/iron/opcode.h
已經加入了對應的指令之外,就要回到 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 {
DEBUG_LOG("func = %s", name);
mrb_func_t func = kh_value(mrb->mt, key);
mrb_value argv[c + 1];
for(int i = 1; i <= c; i++) {
argv[i - 1] = stack[a + i];
}
argv[c] = stack[a + c + 1];
mrb_callinfo ci = { .argc = c + 1, .argv = argv };
mrb->ci = &ci;
func(mrb);
}
NEXT;
}
// ...
這邊我們可以看到實作跟 OP_SEND
幾乎沒有差異,所以是直接複製貼上的。不過多出來的地方是我們對 argc
和 argv
都增加了一個單位,並且把 stack[a + c + 1]
儲存進去,之前有提到參數的範圍是 a + 1
到 a + c
而多出來的這個正好就是傳入的 Block 了!
完成這塊處理後,我們就可以在進行下一步實作 loop
方法,編輯 include/embed_methods.h
加入新的方法實作。
// include/embed_methods.h
// ...
void mrb_loop(mrb_state* mrb);
然後在 hal_arduino
跟 hal_native
下面的 embed_methods.cpp
加入實作。
這邊可以跳過
hal_native
畢竟只適用於在電腦上測試
// src/hal_arduino/embed_methods.cpp
// ...
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];
mrb_exec(mrb, (const uint8_t*)blk.value.p);
}
// ...
如此一來我們就可以順利呼叫到 Block 了,不過似乎還缺少了一些 OPCode 的實作,我們明天再來繼續討論剩下的實作需要哪些東西。