在上一篇我們已經能夠利用 mrb_state
去儲存整個 Ruby VM 運行時共有的狀態,接下來我們要利用 mrb_state
將 Ruby 中呼叫的方法和 C 裡面的方法關聯在一起。
在 mruby 中要實作這個的前提是需要有物件概念的存在,不過我們目前只需要能夠呼叫方法,因此我們需要稍微改變我們的做法簡化這個過程。
關於 mruby 呼叫方法背後實際處理的方式我們在下一篇再做討論
在 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_put
在 mrb_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 的物件跟方法,然後我們再繼續朝向支援傳入參數的目標前進。