經過幾天的努力,我們已經可以在我們自己實作的 Ruby VM 中進行加法的運算,不過到目前爲止都還是停留在透過除錯訊息看到結果的狀況,因此我們要來實作 Ruby 裡面的 puts
方法將上一篇實作的加法結果呈現出來。
大多數的人在入門程式語言時都會是先以印出 Hello World 當作起點,不過實作上相對複雜一點因此我們將他放到後面的步驟來處理。
為了要找出實現我們希望增加的功能所需的 OPCode,我們需要先修改 app.rb
為下面的樣子
puts 10 + 5
下一個缺少的 OPCode 是 OP_SEND
(46
) 也就是方法的呼叫,在 Ruby 中 #puts
屬於 Kernel
物件的方法,所以某種意義上我們在 Ruby 裡面定義的方法其實是在對 Kernel
物件定義,不過在語言底層還有一些特殊的處理,但在沒有定義任何物件的前提下,直接呼叫的方法都會被視為是 Kernel
物件的方法。
所以上一篇提到的
OP_LOADSELF
最初始的物件應該是要放入Kernel
會比較接近原本的設計,不過我們目前並沒有物件的概念存在,因此就直接設定為0
來替代
在 src/opchde.h
加入 OP_SEND
之後,我們繼續在 src/vm.c
裡面補上最原始版本的 OP_SEND
實作。
// ...
switch(op) {
// ...
case OP_SEND:
a = *p++; b = *p++; c = *p++;
// TODO: Always call "puts"
printf("%d\n", ((int)(reg[a + 1])));
break;
// ...
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
在這個實作中,我們直接將數值把它印出來,經過這樣的修改後我們執行 make
大致上會看到如下的輸出
rm -rf bin/*
rm -rf src/app.c
rm -rf src/*.o
mrbc -B app -o src/app.c app.rb
cc -I include -c src/app.c -o src/app.o
cc -I include -c src/irep.c -o src/irep.o
cc -I include -c src/main.c -o src/main.o
cc -I include -c src/vm.c -o src/vm.o
cc -o bin/iron-rb src/app.o src/irep.o src/main.o src/vm.o
DEBUG> locals: 1, regs: 5, ireps: 0
DEBUG> r[1] = self 0
DEBUG> r[2] = 10
DEBUG> r[2] = r[2] + 5
15
DEBUG> return r[1]
RES> 0
首先會發現我們多出了 15
這一行,這是我們利用 printf
將他直接印出來的效果,之後修改 LOG
在非除錯模式隱藏的時候就能看到實際的結果。而昨天實作的 OP_RETURN
原本回傳的數值要是 15
這邊則是變成了 0
這個數值。
這邊大家應該會有幾個疑惑,第一個就是為什麼實際上傳遞給 puts
的數值是 reg[a + 1]
而不是 reg[a]
呢?
這是因為在 mruby 的設計裡面,每個方法呼叫都是對應物件的,而 reg[a]
是指物件本身,也就是 OP_LOADSELF
指向的東西,但是現在他被我們設定成 0
因此就直接被我們跳過不處理,而方法呼叫的資料則會有 N 個參數(Parameters)要傳遞給物件,他就會是 a + 1
到 a + c
這一個範圍,也就是說 mruby 將要當作參數的數值在前面的各種操作(像是 OP_LOADI
)已經依序被放到 reg
陣列裡面,變成一個連續的數值。
接下來的問題肯定就是這次實作的部分並沒有用到 b
和 c
是什麼原因,沒有使用到 c
的原因在前面的段落已經提到,如果我們希望實現 Ruby 中 #puts
多個參數會連續印出多行的效果,可以修改 app.rb
為以下的樣子
puts 1, 2, 3
然後我們調整 src/vm.c
剛剛實作的部分,改為用迴圈處理依序把 a + 1
到 a + c
個數值取出
// ...
switch(op) {
// ...
case OP_SEND:
a = *p++; b = *p++; c = *p++;
// TODO: Always call "puts"
for(int i = 1; i <= c; i ++) {
printf("%d\n", ((int)(reg[a + i])));
}
break;
// ...
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
再次執行 make
之後就會發現畫面上會連續印出 N 個數字,同時也會發現除錯訊息中 nregs
的數量也會稍微的增加,因為我們需要更大的陣列來儲存參數。
透過這樣的實作我們也可以得到一個訊息,假設一個方法的參數(Parameters)過多,是否會在無形中增加遍歷資料的負擔而略為影響效能呢?
最後就是關於 b
數值的使用,我們會在下一篇解釋如何從 IREP 內取出字串、符號解釋細節,在前面說明 IREP 結構的時候有提到除了 ISEQ 之外還有 POOL 和 SYMS 兩個資料區塊用來儲存符號或者變數的初始數值,而這個 b
就是我們用來尋找對應資料的關鍵。
最後要注意一件事情,雖然我們目前沒有用到 b
和 c
的資料,不過 mruby-L1VM 在取值的處理是用 b = *p++
的方式實作,也因此當我們取出資料時同時也會將指標移動到下一個陣列元素上,所以還是要確實的移動三個單位,不然下一次的 OPCode 處理就可能會找到錯誤的指令。