iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 27
0
IoT

拿到錘子的我想在微控制器上面執行 Ruby系列 第 27

Day 27 - Block(一)

之前我們有提到過 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

OP_BLOCK

當我們運行 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_SENDB

當我們時做了 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 幾乎沒有差異,所以是直接複製貼上的。不過多出來的地方是我們對 argcargv 都增加了一個單位,並且把 stack[a + c + 1] 儲存進去,之前有提到參數的範圍是 a + 1a + c 而多出來的這個正好就是傳入的 Block 了!

完成這塊處理後,我們就可以在進行下一步實作 loop 方法,編輯 include/embed_methods.h 加入新的方法實作。

// include/embed_methods.h

// ...
void mrb_loop(mrb_state* mrb);

然後在 hal_arduinohal_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 的實作,我們明天再來繼續討論剩下的實作需要哪些東西。


上一篇
Day 26 - 跑馬燈
下一篇
Day 28 - Block(二)
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言