iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
0
IoT

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

Day 10 - 從 IREP 取出字串

在上一篇我們已經實作到要呼叫方法,但是卻無法從 IREP 中獲取要呼叫的方法名稱。因此我們要實作對應的方法來幫助我們讀取到 IREP 中的 POOL 和 SYMS 裡面所儲存的字串。

在 mruby 的設計中,會把 IREP 解析後儲存到 mrb_irep 的結構中,同時也會將 SYMS 的資料建檔放到記憶體加快讀取,因為我們是運行在微控制器的程式因此先假設記憶體是不太夠用的,先以最原始的方式來處理。

調整 bin_to_uint16 系列方法

目前的進度對應到的是我在 GitHub 上的 Repo feature/5-irep-get 分支,裡面在實作 irep_get 方法後有進行一定程度的重構,這是因為 mruby-L1VM 的方法其實有很多重複的地方,而 mruby 的原始碼巧妙的利用巨集讓這些操作可以用相對簡短乾淨的方式被實現。

首先在 src/opcode.h 裡面加入對讀取 OPCode 有幫助的巨集

// src/opcode.h

/**
 * B = 8bit
 * S = 16bit
 * W = 24bit
 * L = 32bit
 */
#define PEEK_B(pc) (*(pc))
#define PEEK_S(pc) ((pc)[0]<<8|(pc)[1])
#define PEEK_W(pc) ((pc)[0]<<16|(pc)[1]<<8|(pc)[2])
#define PEEK_L(pc) ((pc)[0]<<24|(pc)[1]<<16|(pc)[2]<<8|(pc)[3])

#define READ_B() PEEK_B(p++)
#define READ_S() (p+=2, PEEK_S(p-2))
#define READ_W() (p+=3, PEEK_W(p-3))
#define READ_L() (p+=4, PEEK_L(p-4))

上面的巨集分別對應了幾種不同情境的應用,第一個是給入的指標讀取 8bit、16bit 不等的資料(通常會作為整數回傳),第二組是讀取的處理,也就是我們讀取 1byte 或者 2byte 單位的資料同時「移動指標」到陣列的下一個位置。

需要注意的是這邊直接將 p 作為 READ_ 類型巨集使用的變數,如果執行的方法中沒有定義的話是會出錯的。

// src/irep.h

#include <stdio.h>
#include <stdlib.h>

#include "opcode.h"

// ...

#define IREP_PADDING() p += skip_padding(p)
#define IREP_SKIP_HEADER() READ_L(); READ_S(); READ_S(); READ_S()
#define IREP_SKIP_ISEQ() READ_L(); IREP_PADDING()

// ...

static size_t skip_padding(const uint8_t* buf) {
  const size_t align = sizeof(uint32_t);
  return -(intptr_t)buf & (align - 1);
}

這邊我們在 src/irep.h 裡面加入了一些東西,首先是 skip_padding 方法,在前幾天的處理中我們是直接複製 mruby-L1VM 的實作,這邊我們改成 mruby 版本的實作。另外我們也實作了 IREP_SKIP_ 類型的巨集方便我們在需要跳過 IREP 的 nregs 或者 ISEQ 區段時可以快速處理。

除了 PADDING 處理外其他部分不一定要定義,後面實際上用到的機會其實不多。

最後回到我們的 irep.c 可以先修改之前的 read_irep 方法來習慣一下處理。

// src/irep.c

mrb_irep* read_irep(const uint8_t* src, uint8_t* len) {
  const uint8_t* p = src;

  mrb_irep* irep = (mrb_irep*)malloc(sizeof(mrb_irep));

  READ_L(); // skip record length
  irep->nlocals = READ_S();
  irep->nregs = READ_S();
  irep->rlen = READ_S();

  IREP_SKIP_ISEQ();

  *len = (uint8_t)(p - src);

  return irep;
}

看起來是不是就相對乾淨多了呢?

原本的 skip_padding 是寫在 src/irep.c 裡面的,這次的修改把它放到 src/irep.h 之中。

讀取字串

前置的準備完成後,我們就可以用相對乾淨的方式去實作 irep_get 這個方法,這邊一樣還是參考 mruby-L1VM 的實作,不過因為我們有巨集的加持所以會有一定程度的差異。

mruby-L1VM 有很多算是直接寫死的固定數值,也因此我們需要事後去推測原因。像是 POOL 裡面除了字串之外會有其他數值,也因此會用 1 byte 儲存類型加上 2 byte 儲存長度,在 mruby-L1VM 的實作就會看到 return p + 3; 這樣的寫法。

先在 src/irep.h 增加 irep_get 的方法,以及對應的資料類型編號。

// src/irep.h

#define IREP_TYPE_SKIP    0
#define IREP_TYPE_LITERAL 1
#define IREP_TYPE_SYMBOL  2
#define IREP_TYPE_IREP    3

// ...

const uint8_t* irep_get(const uint8_t* p, int type, int n);
// src/irep.c

// ...

const uint8_t* irep_get(const uint8_t* p, int type, int n) {
  READ_L();
  uint16_t nlocals = READ_S();
  uint16_t nregs = READ_S();
  uint16_t nirep = READ_S();

  // Skip ISEQ
  uint32_t codelen = READ_L();
  IREP_PADDING();
  p += codelen;

  // Find in POOL
  {
    uint32_t npool = READ_L();

    if (type == IREP_TYPE_LITERAL) {
      npool = n;
    }

    for(int i = 0; i < npool; i++) {
      uint8_t type = READ_B();
      uint16_t len = READ_S();
      p += len + 1; // End with null byte
    }

    if (type == IREP_TYPE_LITERAL) {
      return p + 3; // Skip type and length
    }
  }

  // Find in SYM
  {
    uint32_t nsym = READ_L();

    if (type == IREP_TYPE_SYMBOL) {
      nsym = n;
    }

    for (int i = 0; i < nsym; i++) {
      uint16_t len = READ_S();
      p += len + 1; // End with null byte
    }

    if (type == IREP_TYPE_SYMBOL) {
      return p + 2; // Skip length
    }
  }

  // Find in IREP
  {
    if (type == IREP_TYPE_IREP) {
      nirep = n;
    }

    for (int i = 0; i < nirep; i++) {
      p = irep_get(p, IREP_TYPE_SKIP, 0);
    }

    if (type == IREP_TYPE_IREP) {
      return p;
    }
  }

  return p;
}

看起來似乎不太好理解,讀取的邏輯我們在第四天的時候有說明過,不過我們還是拿 SYMS 這塊等一下會用到的部分來詳細的討論一次。

  // Find in SYM
  {
    // 讀取 Symbol 總數
    uint32_t nsym = READ_L();

    // 如果是要讀取 Symbol 的話,設定到要讀取的那一個為止
    if (type == IREP_TYPE_SYMBOL) {
      nsym = n;
    }
    
    // 開始不斷讀取到目標的前一個為止
    for (int i = 0; i < nsym; i++) {
      // len 是這個 Symbol 的長度,像是 `puts` 就會是 `4`
      uint16_t len = READ_S();
      // 跳過 1bytes 因為 `\0` 在 C 作為字串終止的標記 
      p += len + 1;
    }

    // 回傳目標的 Symbol
    if (type == IREP_TYPE_SYMBOL) {
      // 跳過 2bytes 因為是「長度資訊」我們不需要
      return p + 2;
    }
  }

加上註解之後是否更好理解了呢?

因為資料是一個連續的 uint8_t 陣列,因此假設我們想讀取 SYMS 就需要先跳過所有的 POOL 此時利用迴圈不斷讀取到所有的 POOL 資料都被掃過,指標就會自然的指向到 SYMS 的起始點,透過這樣的邏輯我們就能抓取到想要的資料。

至於為什麼要用迴圈而無法一次讀取,是因為我們無法知道每一個資料的長度多少只能依序檢查長度並且跳過,而 IREP 資料的抓取也是類似,只要跳過每一個 IREP 的 POOL / SYMS 區塊 N 次之後就會到達我們想要抓取的 IREP 區塊。

實測 irep_get 方法

最後我們來調整 OP_SEND 的實作,就會發現我們現在可以正確抓取到方法的名稱。

// src/vm.c

// ...

    switch(op) {
      // ...
      case OP_SEND:
        a = *p++; b = *p++; c = *p++;
        const char* func = (const char*)irep_get(data, IREP_TYPE_SYMBOL, b);
        LOG("DEBUG> Call func: %s\n", func);
        // 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;
    }
    
// ...

到目前為止我們就已經做好實作呼叫方法的處理,不過隨著我們要處理的 OPCode 增加,在下一篇我們來重構一下 src/vm.c 讓他更容易擴充吧!


上一篇
Day 9 - 顯示運算結果
下一篇
Day 11 - 重構 VM 處理程序
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言