iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
IoT

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

Day 15 - 方法呼叫資訊

在 mruby 中,我們想知道目前的方法傳入了哪些資訊,會像這樣子使用

mrb_value mrb_puts(mrb_state* mrb, mrb_value self) {
  mrb_int argc = mrb_get_argc(mrb);
  mrb_value* argv = mrb_get_argv(mrb);

  // ...

  return self;
}

在上面的程式碼中,我們實際上並沒有提供任何方法相關的資訊,只需要將 mrb_state 傳入就可以知道現在傳入的參數有幾個並且取得所有的參數。

這是因為 mruby 定義了 mrb_callinfo 資訊,每當我們呼叫 OP_SEND 類型的操作時都會將當下呼叫的資訊封裝,然後放到 mrb_context 裡面保存,用於後續的處理使用。

mrb_context 也是被我們跳過的一部分,這個 Context(情境)可以視為大部分語言的 {} 包起來的範圍,用來限定區域變數跟呼叫等等資訊,方法的呼叫通常會屬於某個 Context。

mrb_callinfo

首先,我們需要定義 mrb_callinfo 讓我們可以在方法呼叫時被使用到,在 src/iron.h 加入新的定義。

// src/iron.h

// ...

typedef struct mrb_callinfo {
  int argc;
  int* argv;
} mrb_callinfo;

因為我們還只能處理整數,所以先假設所有的參數都會是整數,並且用一個陣列的方式將它儲存起來。同時把 mrb_callinfo 放到 mrb_state 裡面讓他可以被 Ruby VM 傳遞這個狀態給其他人。

// src/iron.h

typedef struct mrb_state {
  int exc; /* exception */

  struct kh_mt_s* mt;
  mrb_callinfo* ci;
} mrb_state;

改寫 OP_SEND

我們延續前幾天的實作,再次修改 OP_SEND 的處理,製作一個 mrb_callinfo 並且將他放到 mrb_state 中讓我們可以在呼叫時使用。

// src/vm.c

      CASE(OP_SEND, BBB) {
        // ...
        if (key == kh_end(mrb->mt)) {
          DEBUG_LOG("func %s not found", name);
        } else {
          // ...

          int argv[c];
          for(int i = 1; i <= c; i++) {
            argv[i - 1] = reg[a + i];
          }

          mrb_callinfo ci = { .argc = c, .argv = argv };
          mrb->ci = &ci;

          func(mrb);
        }
        NEXT;
      }

在這邊我們先製作了一個 int 陣列用來存放傳入的數值,然後製作一個 mrb_callinfoargcargv 儲存進去,再將它轉換成一個指標傳遞給 mrb_state 記錄下來。

在後面的測試跑在 ESP8266 時不時會出現記憶體問題而重開機,之前沒有考慮到因為我們是運行在一個「迴圈」內,而 C 語言並不會自己做 GC 而是在方法呼叫結束後釋放記憶體,上面的作法確實有可能造成記憶體洩漏,之後有時間的話會再嘗試改進。

更新自訂方法

上述的處理完成後,我們就可以再次改寫之前定義的 mrb_puts 方法讓他可以接收到多個參數。

void mrb_puts(mrb_state* mrb) {
  int argc = mrb->ci->argc;
  int* argv = mrb->ci->argv;

  for(int i = 0; i < argc; i++) {
    printf("%d\n", argv[i]);
  }
}

如此一來我們再次運行 DEBUG=1 make 的效果,就會跟我們最初實作到 OP_SEND 的成果是相同的,應該會看到類似下面的結果。

[DEBUG] locals: 1, regs: 6, ireps: 0
[DEBUG] r[1] = self 0
[DEBUG] r[2] = 10
[DEBUG] r[2] = r[2] + 5
[DEBUG] r[3] = mrb_int(2)
[DEBUG] r[4] = mrb_int(3)
[DEBUG] func = puts
15
2
3
[DEBUG] return r[1]

mrb_get_argcmrb_get_argv

每次定義方法的時候都要用 mrb->ci->argc 這樣的方式除了容易出錯之外,未來我們擴充 mrb_callinfo 的時候也不容易修改,因此我們可以定義幾個輔助方法來處理。

先在 src/iron.h 中定義新的方法

// src/iron.h

// ...

IRON_API int mrb_get_argc(mrb_state* mrb);
IRON_API int* mrb_get_argv(mrb_state* mrb);

然後加入方法的實作到 src/iron.c 裡面

// src/iron.c

IRON_API
int mrb_get_argc(mrb_state* mrb) {
  return mrb->ci->argc;
}

IRON_API
int* mrb_get_argv(mrb_state* mrb) {
  return mrb->ci->argv;
}

之後如果我們增加了 mrb_context 的機制,就可以直接針對這兩個方法處理,而不需要去改寫所有定義過的方法。

下一篇我們會來實作 mrb_value 也就是實際上將變數的概念加入到我們的 Ruby VM 之中,在 mruby-L1VM 的設計裡面嚴格上來說是不存在這個概念的,取而代之的是直接用指標或者整數來儲存,不過這樣實作一些處理的時候反而會不太容易操作。


上一篇
Day 14 - mruby 的物件與方法
下一篇
Day 16 - 變數(ㄧ)
系列文
拿到錘子的我想在微控制器上面執行 Ruby30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言