iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 9
0
IoT

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

Day 9 - 顯示運算結果

經過幾天的努力,我們已經可以在我們自己實作的 Ruby VM 中進行加法的運算,不過到目前爲止都還是停留在透過除錯訊息看到結果的狀況,因此我們要來實作 Ruby 裡面的 puts 方法將上一篇實作的加法結果呈現出來。

大多數的人在入門程式語言時都會是先以印出 Hello World 當作起點,不過實作上相對複雜一點因此我們將他放到後面的步驟來處理。

為了要找出實現我們希望增加的功能所需的 OPCode,我們需要先修改 app.rb 為下面的樣子

puts 10 + 5

OP_SEND

下一個缺少的 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 這個數值。

mruby 的方法呼叫

這邊大家應該會有幾個疑惑,第一個就是為什麼實際上傳遞給 puts 的數值是 reg[a + 1] 而不是 reg[a] 呢?

這是因為在 mruby 的設計裡面,每個方法呼叫都是對應物件的,而 reg[a] 是指物件本身,也就是 OP_LOADSELF 指向的東西,但是現在他被我們設定成 0 因此就直接被我們跳過不處理,而方法呼叫的資料則會有 N 個參數(Parameters)要傳遞給物件,他就會是 a + 1a + c 這一個範圍,也就是說 mruby 將要當作參數的數值在前面的各種操作(像是 OP_LOADI)已經依序被放到 reg 陣列裡面,變成一個連續的數值。

接下來的問題肯定就是這次實作的部分並沒有用到 bc 是什麼原因,沒有使用到 c 的原因在前面的段落已經提到,如果我們希望實現 Ruby 中 #puts 多個參數會連續印出多行的效果,可以修改 app.rb 為以下的樣子

puts 1, 2, 3

然後我們調整 src/vm.c 剛剛實作的部分,改為用迴圈處理依序把 a + 1a + 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 就是我們用來尋找對應資料的關鍵。

最後要注意一件事情,雖然我們目前沒有用到 bc 的資料,不過 mruby-L1VM 在取值的處理是用 b = *p++ 的方式實作,也因此當我們取出資料時同時也會將指標移動到下一個陣列元素上,所以還是要確實的移動三個單位,不然下一次的 OPCode 處理就可能會找到錯誤的指令。


上一篇
Day 8 - 加法 OPCode 處理
下一篇
Day 10 - 從 IREP 取出字串
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言