蒼時弦也
蒼時弦也
資深軟體工程師
發表於

RITE 的結構 - mruby-go

這篇文章是 mruby-go 系列的一部分。

mruby 透過編譯器(Compiler,通常是 mrbc)編譯後,會產生 mrb 格式的二進位檔案,這個檔案的格式被稱作 RITE 如果要運行編譯後的 mruby 程式碼,就需要能夠解析並且讀取。

結構

RITE 的結構大致上分為兩個部分,rite_binary_header 以及數個 Section(區段)所組成,rite_binary_header 會紀錄這個二進位檔案的格式、mruby 版本、編譯器資訊等等。

區段基本上分為四種,分別是 irepdebuglv(Local Variable)、footer(無意義)四個類型,如果沒有特別指定要包含 debug 區段,實際上只會有另外三個類型。

每個區段都會有自己的 Section Header 用來識別,除了 irep 還會有額外的 rite_version 的版本資訊外,其他區段都由 char[4] 的識別(Identity,ident)和 uint32 的區段大小所組成。

1// example irep section
2struct rite_section_irep_header {
3  uint8_t section_ident[4];
4  uint8_t section_size[4];
5  uint8_t rite_version[4];
6};

了解這些後,我們就能夠用 Golang 讀取這些資訊。

BinaryHeader

讀取 rite_binary_header 的方式並不困難,我們只需要定義一個 BinaryHeader 的結構,並且使用 Sized Bytes(固定大小的 byte 陣列)就可以讓 binary 套件的 Read 方法正確的將數值填入。

 1type BinaryHeader struct {
 2    Identifier [4]byte
 3	Version    struct {
 4		Major [4]byte
 5		Minor [4]byte
 6	}
 7	Size       uint32
 8	Compiler   struct {
 9		Name    [4]byte
10		Version uint32
11	}
12}
13
14func ReadHeader(r io.Reader) (*BinaryHeader, error) {
15	header := &BinaryHeader{}
16	err := binary.Read(r, binary.BigEndian, header)
17	if err != nil {
18		return nil, err
19	}
20	return header, nil
21}

Sections

讀取區段的方式比較複雜,會在後續的文章依序介紹。如果想要區分出一個區段,我們可以加入 SectionHeader 結構,將共用的區段資訊提取出來。

 1type SectionHeader struct {
 2	Identity [4]byte
 3	Size     uint32
 4}
 5
 6func ReadeSection(r io.Reader, remain uint32) (*Section, error) {
 7	header := &SectionHeader{}
 8	err := binary.Read(r, binary.BigEndian, header)
 9	if err != nil {
10		return nil, err
11	}
12
13	isOverSize := header.Size > remain
14	if isOverSize {
15		return nil, errors.New("section size is larger than binary")
16	}
17
18	// ...
19
20    sectionHeaderSize = uint32(unsafe.SizeOf(SectionHeader{}))
21    noopBuffer := make([]byte, header.Size - sectionHeaderSize)
22    _, err := r.Read(noopBuffer)
23    if err != nil {
24      return ni, err
25    }
26
27    return section, nil
28}

在虛擬機器運作中,不論是 BinaryHeader 或者 SectionHeader 都是非必要的資訊,因此在實際的實作中會拋棄這些資料。

在 RITE 的設計中,Header 所包含的大小資訊會包含 Header 的大小,因此在計算的時候需要扣掉 Header 的大小,在上述的範例中我們會用 header.Size - sectionHeaderSize 來當做要製作的 noopBuffer 陣列的大小,確保 io.Reader 讀取時可以正確地停在下一個區段的起始點。

在 C 語言中的陣列可以透過指標(Pointer)的特性切換到任意的位置處理,然而在 Golang 中被封裝成 io.Reader 後我們只能依序讀取,那麼讀取的游標(Cursor)停止的位置就變得非常重要。


mruby-go 是完全使用 Golang 來實現 mruby 的計畫,預期讓 Golang 可以運行 Ruby 來實現像是 DSL 或是 Hook 這類處理,讓開發有更多的彈性。