iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
0

在上一篇我們已經能夠利用 mrb_state 去儲存整個 Ruby VM 運行時共有的狀態,接下來我們要利用 mrb_state 將 Ruby 中呼叫的方法和 C 裡面的方法關聯在一起。

在 mruby 中要實作這個的前提是需要有物件概念的存在,不過我們目前只需要能夠呼叫方法,因此我們需要稍微改變我們的做法簡化這個過程。

關於 mruby 呼叫方法背後實際處理的方式我們在下一篇再做討論

khash

在 mruby 裡面會看到一個叫做 khash.h 的檔案,他除了被用來實作 Hash 物件之外,也被用來處理 Ruby 中方法跟 C 裡面的方法的關聯。因為之前沒有實作過類似的資料結構,因此我們就直接參考 mruby 的作法也使用 khash 來實現 Key-Value 的資料結構,這次我們會用 klib 提供的 khash.h 來實作。

不過 mruby 裡面的 khash.h 跟上面的版本不太一樣,這邊的 khash 應該是某一種演算法的實作,在實驗的時候並沒有深入去探討這個問題。不過 klib 提供的 khash 還是有蠻多人使用的,因此我們只需要參考官網的範例就可以大製作出屬於我們自己的 Hash 結構。

為了要將授權資訊一起保存,因此我會習慣用 git submodule 的方式去引用第三方套件,不過 khash.h 在檔案開頭已經有將授權資訊寫上,所以大家可以直接把它下載到 src 目錄下面使用。

初始化

我們先修改 src/iron.h 在裡面將 khash 進行初始化,並且設定我們要的 Key-Value 結構為何。

// src/iron.h

// ...

#include <khash.h>

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

KHASH_MAP_INIT_STR(mt, mrb_func_t)

這邊我們使用 KHASH_MAP_INIT_STR 這個巨集定義了一個叫做 mt(Method Table) 的 Map 類型結構,同時設定儲存的資料類型是一個 mrb_func_t 的資料,而這個 mrb_func_t 是一個 Function Pointer(方法指標)在前一行我們用 typedef 定義了符合條件的方法會傳入一個 mrb_state* 作為參數。

透過這樣的設定,我們就可以透過 khash 產生一個 Map 類型的結構,同時 Key 是 String 並且 Value 是 mrb_func_t 類型的資料,這樣就能夠用一段字串找到一個方法。

有了結構之後,我們再去修改原本 mrb_state 的結構定義,將 mt 存加入到 mrb_state 裡面。

// src/iron.h

// ...

typedef struct mrb_state {
  int exc; /* exception */
  struct kh_mt_s *mt;
} mrb_state;

// ...

因為我們在 mrb_state 儲存的是一個 mt 資料的指標因此要再修改 mrb_open 將這個資料結構初始化。

// src/iron.c

IRON_API
mrb_state* mrb_open(void) {
  static const mrb_state mrb_state_zero = { 0 };
  mrb_state* mrb = (mrb_state*)malloc(sizeof(mrb_state));
  *mrb = mrb_state_zero;

  mrb->mt = kh_init(mt);

  return mrb;
}

使用 khash 算是相對容易的,我們只做了很少的修改就將所需要的調整處理完畢。

定義方法

回到 src/iron.h 我們加入 mrb_define_method 這個方法。

// src/iron.h

// ...
IRON_API void mrb_define_method(mrb_state* mrb, const char* name, mrb_func_t func);

這邊一樣是簡化的版本,在 mruby 裡面要定義方法需要指定 Class 但是因為我們沒有實作物件的概念,因此就直接跳過這個設定值。除此之外我們也不對方法的參數做任何檢查,即使傳入了預期之外的參數也不會出現任何問題。

接著我們到 src/iron.c 來實作定義方法的實際行為

// src/iron.c

// ...

void mrb_define_method(mrb_state* mrb, const char* name, mrb_func_t func) {
  int ret;
  khiter_t key = kh_put(mt, mrb->mt, name, &ret);
  kh_value(mrb->mt, key) = func;
}

我們實作的處理非常的單純,先用 kh_putmrb_state 的 Method Table 製作一個 key 出來,然後再利用 kh_value 找到實際的值將他設定為我們要連結的 Function Pointer 即可。

這邊我們先不考慮重複定義之類的問題,單純以「可以將方法連結」當作目標,因此直接無視了 kh_put 的結果,以及不對是否有存在的值做處理。

關於 khash 的運作這邊就不多做討論,有興趣的話可以去讀看看 khash.h 的原始碼來了解他的運作原理。

呼叫方法

當我們將對應的方法資訊記錄下來後,我們就可以到 OP_SEND 進行修改,結合我們前幾天抓取到的方法名稱來做處理。

// src/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);
        } else {
          DEBUG_LOG("func = %s", name);
          mrb_func_t func = kh_value(mrb->mt, key);
          func(mrb);
        }
        NEXT;
      }

// ...

修改後的 OP_SEND 會利用 khash 提供的 API 去尋找是否有存在的值,如果沒有的話我們會出出 func xxx not found 的除錯訊息,不然就會直接去呼叫取出的 Function Pointer 並且直接呼叫他。

註冊方法

前面的步驟都完成後,我們就可以把自訂的方法註冊到我們的 Method Table 上面了。

// src/main.c

#include <stdio.h>

#include "app.h"
#include "vm.h"
#include "iron.h"

void mrb_puts(mrb_state* mrb) {
  printf("Hello World\n");
}

int main(int argc, char** argv) {

  mrb_state* mrb = mrb_open();
  mrb_define_method(mrb, "puts", mrb_puts);
  mrb_run(mrb, app);
  mrb_close(mrb);

  return 0;
}

使用 make 執行我們的 Ruby VM 就可以看到 Hello World 的訊息被印出來,雖然目前無法處理傳入的參數但是已經能夠在 Ruby 裡面增加我們由 C 語言所定義的方法。

下一篇會簡單討論 mruby 的物件跟方法,然後我們再繼續朝向支援傳入參數的目標前進。


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

尚未有邦友留言

立即登入留言