經過前面幾天的分析,我們目前已經有一個可以撰寫 Ruby VM 的環境,也了解該如何從 mruby 的機器碼中讀取到實際上要執行的 ISEQ 區段,這篇文章會先進行第一階段,也就是讀取 IREP 資訊用於後續處理的部分。
為了能讓我們的 app.rb
可以事先轉換成能夠被包含在 C 原始碼裡面的版本,我們需要對原本的 Makefile 做一些更新,加入轉換 Ruby 程式碼為 .c
的機制。
# 手動加入 `src/app.c` 作為任務的相依
%.o: %.c src/app.c
$(CC) -I include -c $< -o $@
# src/app.c 這個檔案會基於 app.rb 透過 mrbc 產生
src/app.c: app.rb
mrbc -B app -o $@ $^
# 加入 `src/app.o` 作為任務的相依,檔案由 `src/app.o` 任務生成
bin/iron-rb: $(OBJECTS) src/app.o
@mkdir -p $(BIN_DIR)
$(CC) -o $@ $^
# 增加刪除 `src/app.c` 的指令
clean:
rm -rf $(BIN_DIR)/*
rm -rf src/app.c
rm -rf src/*.o
因為 src/app.c
並不是普通的 .c
原始碼而是透過 mruby 的編譯器產生的檔案,因此我們需要加入對應的任務來告知獲取這個檔案的方法。
mrbc 可以利用
-B
選項輸出成 C 語言的格式,而-B
後的參數則是變數名稱,這邊我們用app
作為變數名稱。
因為生成的是 .c
的原始碼檔案,我們還需要一個 .h
檔案才可以讀取到裡面的機器碼,因此需要再新增另一個 src/app.h
來輔助。
#ifndef _IRON_APP_H_
#define _IRON_APP_H_
#include <stdint.h>
extern uint8_t app[];
#endif
如此一來我們就可以在我們的專案中用 #include "app.h"
來讀取到 mruby 編譯出來的機器碼。
除了這個方法之外,我們還可以透過
#include "app.c"
的方式來讀取,不過比較好的做法是直接輸出為src/app.h
就不用多製作一個檔案,在比較後面的步驟有修正為這個方式。
因為我們跟 mruby-L1VM 的目標不同,我們希望製作的是一個比 mruby 本身精簡,但又不像 mruby-L1VM 省略大多數功能的方式設計,因此會稍微做簡單的檔案區分,除此之外將檔案分開專寫也有助於其他人閱讀原始碼。
// src/irep.h
#ifndef _IRON_IREP_H_
#define _IRON_IREO_H_
/**
* mruby binary header
*
* uint8_t rite_binary_header[22]
* uint8_t rite_section_irep_header[12]
*
*/
#define irep_load(irep) ((irep) + 34)
#endif
首先,我們先參考 mruby-L1VM 的方式,製作一個跳過 mruby 的 RITE Header 和 IREP Header 的處理,直接進入到 IREP 資料的本體的 irep_load
巨集。
// src/vm.h
#ifndef _IRON_VM_H_
#define _IRON_VM_H_
#include <stdint.h>
#include "irep.h"
// TODO: pass mrb_state
#define mrb_run(irep) mrb_exec(irep_load(irep))
int mrb_exec(const uint8_t* irep);
#endif
然後我們再定義一個 mrb_run
巨集來當作執行的進入點,同時定義 mrb_exec
作為我們運行程式的本體,也是我們之後會讀取 ISEQ 執行的地方。
#include "vm.h"
int mrb_exec(const uint8_t* irep) {
// TODO
return 0;
}
最後我們將 mrb_exec
的實作放到定位先不做任何處理。
完成上述步驟後,我們將 main.c
更新去呼叫這些 API 順便驗證前面的實作是沒有問題的。
#include "app.h"
#include "vm.h"
int main(int argc, char** argv) {
mrb_run(app);
return 0;
}
下一篇會開始針對 IREP 的讀取進行處理,我們會參考 mruby-L1VM 來讀取 IREP 的 nlocals
、nregs
等資訊。