iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 29
0
IoT

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

Day 29 - 回傳值

雖然我們實作了好幾個方法,但忘記了要處理方法回傳值,在 mruby 中處理回傳值也是相當簡單的,因為編譯器在生成指令的時候都已經幫我們處理好對應的機制,只需要將數值複製到正確的位置上就自然而然能夠正常的將數值回傳。

修改 mrb_func_t

我們原本在 lib/iron/iron.h 定義的 mrb_func_t 是像這樣

typedef void (*mrb_func_t)(mrb_state* mrb);

在 mruby 的設計中每個方法的定義都應該回傳 mrb_value 作為回傳值,也因此我們需要將他修正為

typedef mrb_value (*mrb_func_t)(mrb_state* mrb);

修改方法

因為修改了 mrb_func_t 也因此之前定義的方法都會變成無法使用的狀態,所以需要修正方法的實作,這邊用 mrb_puts 當作示範。

mrb_value mrb_puts(mrb_state* mrb) {
  int argc = mrb_get_argc(mrb);
  mrb_value* argv = mrb_get_argv(mrb);

  for(int i = 0; i < argc; i++) {
    if(argv[i].tt == MRB_TT_STRING) {
      printf("%s\n", mrb_string(argv[i]));
    } else {
      printf("%d\n", mrb_fixnum(argv[i]));
    }
  }

  return mrb_nil_value();
}

實際上並不困難,只需要修改回傳值以及加入 return mrb_nil_value() 回傳即可。

在實際的 mruby 實作(CRuby 也是類似)還會傳入 mrb_value self 當作參數,大多數時候都會是 return self 但因為我們沒有實作物件的概念,因此即使回傳 self 也會是 nil

處理回傳值

接下來我們需要把 OP_SEND 相關的處理加上修正,讓回傳值可以正確的被存到寄存器裡面。

// lib/iron/vm.c

// ...
      CASE(OP_SEND, 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);
          stack[a] = mrb_nil_value();
        } else {
          // ...

          stack[a] = func(mrb);
        }
        NEXT;
      }
      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);
          stack[a] = mrb_nil_value();
        } else {
          // ...
          
          stack[a] = func(mrb);
        }
        NEXT;
      }
// ...

實際上就是將找不到方法時直接回傳 nil 確保不會抓取到錯誤的數值,同時將剛剛修改過的 mrb_func_t 回傳值儲存到原本要用來儲存回傳值的 stack[a] 上面。

修改 mrb_exec

經過前面實作 Block 的經驗,我們會發現一個 Block 的運行單位剛好是 mrb_exec 而呼叫 return 的時候也剛好是從 mrb_exec 離開,因此我們也要將 mrb_exec 的回傳值修改為 mrb_value 搭配其他機制。

先把原本在 lib/iron/vm.hint 替換為 mrb_value

mrb_value mrb_exec(mrb_state* mrb, const uint8_t* irep);

然後修改實作將回傳值以及用到 return 的地方都用 mrb_value 替代

// lib/iron/vm.c

mrb_value mrb_exec(mrb_state* mrb, const uint8_t* data) {
 // ...
      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 stack[a];
      }
 
 // ...
 
 return mrb_nil_value();
}

如此一來我們就可以取用回傳值來做一些處理了!

加法測試

為了驗證前面的修改能夠正常使用,我們實做一個簡單的 mrb_add 方法來確認。

sum = add(1, 2)
puts sum
mrb_value mrb_add(mrb_state* mrb) {
  mrb_value* argv = mrb_get_argv(mrb);

  int sum = mrb_fixnum(argv[0]) + mrb_fixnum(argv[1]);

  return mrb_fixnum_value(sum);
}

// ...

mrb_define_method(mrb, "add", mrb_add);

調整測試的程式碼加入上面兩段程式後,運行 pio test 就可以看到 3 被正確的顯示在螢幕上。

要注意上面的程式碼只是參考不能直接使用

不過這個回傳值處理還是非常陽春的,像是「陣列」就無法處理,這些情況就需要未來持續擴充我們的 Ruby VM 實作來支援。


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

尚未有邦友留言

立即登入留言