在 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
讓我們可以在方法呼叫時被使用到,在 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
的處理,製作一個 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_callinfo
將 argc
和 argv
儲存進去,再將它轉換成一個指標傳遞給 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_argc
和 mrb_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 的設計裡面嚴格上來說是不存在這個概念的,取而代之的是直接用指標或者整數來儲存,不過這樣實作一些處理的時候反而會不太容易操作。